mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 03:27:08 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49410ad867 | ||
|
|
3689dca688 | ||
|
|
671a00b1ac | ||
|
|
9096ec03a7 | ||
|
|
5fec493960 | ||
|
|
a6a4fff2aa |
No files matched your search
@@ -36,18 +36,3 @@ updates:
|
||||
rust-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: uv
|
||||
directory: /cmake/scripts/codegen
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
time: "04:00"
|
||||
timezone: Etc/GMT
|
||||
commit-message:
|
||||
prefix: "chore: [DEPENDABOT] "
|
||||
target-branch: develop
|
||||
groups:
|
||||
codegen-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -174,6 +174,7 @@ test.server > xrpl.basics
|
||||
test.server > xrpl.config
|
||||
test.server > xrpld.app
|
||||
test.server > xrpld.core
|
||||
test.server > xrpld.rpc
|
||||
test.server > xrpl.json
|
||||
test.server > xrpl.protocol
|
||||
test.server > xrpl.server
|
||||
|
||||
@@ -22,6 +22,15 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
|
||||
|
||||
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- A request echoed back in an error reply now has every credential-bearing field masked: `admin_password`, `admin_user`, `passphrase`, `password`, `secret`, `seed`, `seed_hex`, `url_password`, `url_username` and `username`. Nesting no longer matters, so a credential inside `params` is masked too. The same masking is applied to every request and reply written to the log, and it covers six further names that only a reply carries: `master_key`, `master_seed`, `master_seed_hex`, `validation_key`, `validation_private_key` and `validation_seed`, which is how `wallet_propose` and `validation_create` used to write a live private key to the log. A request or reply written to the log is truncated at 10,000 characters. An `[rpc_startup]` line that does not parse is reported by its position in the section and the parser's message, never by its text, and no command runs for it.
|
||||
- The command line client no longer prints a credential the operator did not type. A failing command echoes the request it built under `request_sent`, which carries the `admin_password` the client copies out of `[port_rpc]` in the config, so `./xrpld account_info rBogus` printed that password to stdout and into any captured output. `request_sent` is now masked. The `rpc` member beside it, which echoes the arguments as they were typed, is unchanged. The command line client also no longer writes an unparsed `json` or `ripple_path_find` argument to its trace log before parsing it, where a `secret` inside that argument could not be masked; it logs the parsed request instead, masked. The reply it receives is logged the same way, parsed and masked, where the raw body was written before, a `validation_create` answer included.
|
||||
- A WebSocket frame that does not parse, or exceeds the request size limit, is answered `{"type": "error", "error": "jsonInvalid", "size": <bytes>}`. The frame's body is reported by size rather than echoed back in a `value` member, since a body that does not parse has no fields to mask. A client that read `value` gets `size` instead.
|
||||
- Four error codes that named no HTTP status of their own, and so answered 200 on a reply reporting an error, now name one: `actMalformed`, `alreadyMultisig` and `alreadySingleSig` answer 400, and `actNotFound` answers 404. **No shipped envelope reports these four.** A request sending `ripplerpc: "3.0"` still receives 200 for all four, as it always has, so `account_info` on a malformed account or one the ledger does not hold answers 200 exactly as before.
|
||||
|
||||
## XRP Ledger server version 3.5.0
|
||||
|
||||
Version 3.5.0 is not yet released.
|
||||
|
||||
@@ -10,4 +10,4 @@ pcpp>=1.30
|
||||
pyparsing>=3.0.0
|
||||
|
||||
# Template engine - used to generate C++ code from templates
|
||||
Mako>=1.4.2
|
||||
Mako>=1.2.2
|
||||
@@ -1,8 +1,8 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.in --generate-hashes --output-file requirements.txt
|
||||
mako==1.4.3 \
|
||||
--hash=sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f \
|
||||
--hash=sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a
|
||||
mako==1.3.12 \
|
||||
--hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \
|
||||
--hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a
|
||||
# via -r requirements.in
|
||||
markupsafe==3.0.3 \
|
||||
--hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
@@ -198,6 +200,15 @@ public:
|
||||
*/
|
||||
Value(StaticString const& value);
|
||||
Value(std::string const& value);
|
||||
/**
|
||||
* @brief Constructs a value from a string view.
|
||||
*
|
||||
* The characters are copied, so the view need not outlive the call and need
|
||||
* not be NUL-terminated.
|
||||
*
|
||||
* @param value The characters to copy.
|
||||
*/
|
||||
Value(std::string_view value);
|
||||
Value(bool value);
|
||||
Value(Value const& other);
|
||||
~Value();
|
||||
@@ -472,6 +483,32 @@ toJson(xrpl::Number const& number)
|
||||
bool
|
||||
operator==(Value const&, Value const&);
|
||||
|
||||
/**
|
||||
* Compares a value with a string view, reading the value's characters in place
|
||||
* rather than building a Value from the view.
|
||||
*
|
||||
* Constrained to the exact type: a string literal converts equally well to a
|
||||
* view and to a Value, so a plain overload makes `value == "literal"`
|
||||
* ambiguous.
|
||||
*
|
||||
* @param x The value to compare.
|
||||
* @param y The characters to compare it against.
|
||||
* @return Whether `x` is a string whose characters up to its first NUL are
|
||||
* exactly the characters of `y`.
|
||||
*/
|
||||
template <class T>
|
||||
requires std::same_as<T, std::string_view>
|
||||
bool
|
||||
operator==(Value const& x, T y)
|
||||
{
|
||||
if (!x.isString())
|
||||
return false;
|
||||
|
||||
// A string `Value` can hold a null pointer, which names no characters, so it equals no view.
|
||||
char const* const s = x.asCString();
|
||||
return s != nullptr && std::string_view{s} == y;
|
||||
}
|
||||
|
||||
bool
|
||||
operator<(Value const&, Value const&);
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
@@ -19,216 +17,6 @@ namespace xrpl {
|
||||
|
||||
class STTx;
|
||||
|
||||
/**
|
||||
* @brief Return the Vault's current assets total.
|
||||
*
|
||||
* FixedPrecision Vaults return AssetsAvailable plus AssetsDeployed, with no
|
||||
* rounding to the asset's 16-digit precision: the sum is exact at Number's
|
||||
* active 19-digit mantissa width (the large mantissa range, enabled by
|
||||
* featureSingleAssetVault or featureLendingProtocol), rounds Downward regardless of the
|
||||
* caller's ambient mode beyond that, and is not rounded to match the cached
|
||||
* sfAssetsTotal field. Legacy and CashBasis Vaults return the stored
|
||||
* AssetsTotal.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @return The current assets total.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* @brief A change to a FixedPrecision Vault's balances.
|
||||
*/
|
||||
struct VaultBalanceChange
|
||||
{
|
||||
/**
|
||||
* Signed change to AssetsAvailable. Must be the exact amount moved on the
|
||||
* Vault pseudo-account in the same transaction. Absent means no cash
|
||||
* moved; adjustVaultBalances then uses zero of the Vault's own asset.
|
||||
*/
|
||||
std::optional<STAmount> cash = std::nullopt;
|
||||
|
||||
/**
|
||||
* Signed change to AssetsDeployed: open Loan principal, exact.
|
||||
*/
|
||||
Number deployed = 0;
|
||||
|
||||
/**
|
||||
* Signed change to scheduled, unpaid interest (YieldUnrealized), exact.
|
||||
*/
|
||||
Number yield = 0;
|
||||
|
||||
/**
|
||||
* Signed change to impaired principal (LossUnrealized), exact.
|
||||
*/
|
||||
Number loss = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Apply a balance change to a FixedPrecision Vault. The only writer of
|
||||
* AssetsAvailable, AssetsDeployed, AssetsTotal, YieldUnrealized and
|
||||
* LossUnrealized on these Vaults after creation.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param change The balance change to apply.
|
||||
* @param j Journal for logging the fatal (tefBAD_LEDGER) case.
|
||||
*
|
||||
* @return tesSUCCESS; tecLIMIT_EXCEEDED if LossUnrealized would exceed
|
||||
* AssetsDeployed; tefBAD_LEDGER if AssetsAvailable, AssetsDeployed,
|
||||
* LossUnrealized or YieldUnrealized would become negative.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j);
|
||||
|
||||
/**
|
||||
* Return the Vault's current live exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use the exponent of AssetsTotal. FixedPrecision
|
||||
* Vaults use max(base exponent, exponent of AssetsTotal): the grid is never
|
||||
* finer than -Scale and coarsens once AssetsTotal outgrows 16 digits there.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Return the Vault's base exponent.
|
||||
*
|
||||
* Legacy and CashBasis Vaults use their current live exponent. FixedPrecision
|
||||
* Vaults use -Scale, or 0 for integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Round an amount at the Vault's posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
namespace detail {
|
||||
|
||||
/**
|
||||
* Return the Vault's posterior live exponent after applying an unrounded delta.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Shared grid-floor core of getVaultScale / getPosteriorVaultScale /
|
||||
* getPosteriorBrokerCoverScale: baseScale when reference is zero, otherwise
|
||||
* max(baseScale, scale(reference, asset)), rounded Downward regardless of
|
||||
* the caller's ambient rounding mode.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale);
|
||||
|
||||
/**
|
||||
* Shared core of getPosteriorVaultScale and getPosteriorBrokerCoverScale:
|
||||
* the exponent reference + delta would have under version's live-scale rule
|
||||
* (floored at baseScale for FixedPrecision, unbounded for Legacy/CashBasis).
|
||||
*
|
||||
* @param version The Vault's LEVersion.
|
||||
* @param asset The Vault's underlying asset.
|
||||
* @param baseScale The Vault's base exponent; unused outside the
|
||||
* FixedPrecision case.
|
||||
* @param reference The balance the delta is applied to (AssetsAvailable,
|
||||
* AssetsTotal, or a LoanBroker's CoverAvailable).
|
||||
* @param delta The unrounded change to reference.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta);
|
||||
|
||||
/**
|
||||
* Shared core of creditToPosteriorAvailableScale and
|
||||
* creditToPosteriorBrokerCoverScale: floors reference + raw at scale and
|
||||
* returns the difference from reference, so the delta applied is exactly
|
||||
* what moves reference onto the floored sum. See
|
||||
* creditToPosteriorAvailableScale's doc for why the sum, not raw alone,
|
||||
* must be floored. raw is the exact amount; it is not rounded on its own
|
||||
* before being added to reference.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int atScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Round a FixedPrecision cash inflow (LoanPay's credit, LoanManage's
|
||||
* default cover credit) into AssetsAvailable, never AssetsTotal, which is
|
||||
* a derived cache, by flooring the sum rather than the delta:
|
||||
* credit equals floor16(AssetsAvailable + raw) minus AssetsAvailable, never
|
||||
* finer than the Vault's base exponent (-Scale).
|
||||
*
|
||||
* Flooring only the delta at its own posterior grid is not enough: the sum
|
||||
* can still need a 17th digit when AssetsAvailable is off the coarser grid
|
||||
* right after crossing a power of ten. Example at Scale 6: AssetsAvailable
|
||||
* equals 9999999999.999999 (16 digits already) and a credit of 0.000011
|
||||
* floors to 0.00001 on its own (finer) grid, but 9999999999.999999 plus
|
||||
* 0.00001 equals 10000000000.000009, which still needs 17 digits. Flooring
|
||||
* the sum instead, floor16(9999999999.999999 + 0.000011) minus
|
||||
* 9999999999.999999, is exact by construction, since STAmount's own
|
||||
* 16-digit canonical form of the sum is what gets subtracted from.
|
||||
*
|
||||
* raw is taken as an exact Number: rounding it to 16 digits before adding
|
||||
* it to AssetsAvailable could drop a tail that moves the floored sum.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode);
|
||||
|
||||
/**
|
||||
* Open-zone capacity ceiling: 9 * 10^(15 + baseScale).
|
||||
*
|
||||
* Defined only for FixedPrecision Vaults, where this is 9 * 10^(15 - P).
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Check whether amount is an admissible optional inflow.
|
||||
*
|
||||
* Legacy and CashBasis Vaults always succeed. FixedPrecision Vaults must
|
||||
* remain at their base scale after applying the rounded amount, and the
|
||||
* posterior capacity (AssetsTotal + YieldUnrealized + rounded amount) must
|
||||
* stay within the Open zone.
|
||||
*
|
||||
* The amount is rounded toward zero at the posterior live exponent.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Open-zone capacity after adding roundedAmount: AssetsTotal + YieldUnrealized
|
||||
* + roundedAmount, computed TowardsZero regardless of the caller's ambient
|
||||
* rounding mode. Defined only for FixedPrecision Vaults.
|
||||
*
|
||||
* Shared by checkOptionalVaultInflow and LoanSet's loan-origination capacity
|
||||
* check, which pair this formula with different scale checks (posterior vs.
|
||||
* current), so only the capacity formula itself -- not the whole check -- is
|
||||
* shared here.
|
||||
*
|
||||
* @param vault The vault SLE. Must be FixedPrecision.
|
||||
* @param roundedAmount The amount to add, already rounded to the relevant
|
||||
* posterior or base scale by the caller.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount);
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -258,81 +46,31 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& shares);
|
||||
|
||||
/**
|
||||
* Adjusts a requested asset change (delta) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures sfAssetsTotal, sfAssetsAvailable,
|
||||
* Adjusts a requested asset change (`delta`) to match the decimal scale of the
|
||||
* updated total vault assets. This ensures `sfAssetsTotal`, `sfAssetsAvailable`,
|
||||
* and the actual asset transfer change by the exact same representable amount.
|
||||
*
|
||||
* Rounding strategy:
|
||||
* - Legacy/CashBasis debits (withdrawals): rounds down the magnitude of delta
|
||||
* on the new scale to prevent paying out more than requested.
|
||||
* - Legacy/CashBasis credits: floors the resulting total asset balance and
|
||||
* returns the difference from the current total.
|
||||
* - FixedPrecision credits only (VaultDeposit): rounds the delta toward zero
|
||||
* at the scale getAssetsTotal (AssetsAvailable + AssetsDeployed, derived,
|
||||
* not the stored AssetsTotal cache) would have after applying delta.
|
||||
* FixedPrecision outflows (VaultWithdraw, VaultClawback) never call this;
|
||||
* they use clampVaultOutflow instead.
|
||||
* - Debits (withdrawals): Rounds down `|delta|` on the new scale to prevent
|
||||
* paying out more than requested.
|
||||
* - Credits (deposits): Floors the resulting total asset balance and returns the
|
||||
* difference from the current total. This prevents crediting the vault with
|
||||
* more assets than the user deposited.
|
||||
*
|
||||
* Key rules:
|
||||
* - The returned magnitude never exceeds the magnitude of delta.
|
||||
* - Returns tecPRECISION_LOSS if the change is smaller than 1 ULP of the target scale
|
||||
* - The returned magnitude never exceeds `|delta|`.
|
||||
* - Returns `tecPRECISION_LOSS` if the change is smaller than 1 ULP of the target scale
|
||||
* (prevents share operations when totals cannot change).
|
||||
* - For integer assets (XRP, MPT), rounding is a no-op.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed change to sfAssetsTotal.
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* @return The rounded, positive magnitude, or `tecPRECISION_LOSS` if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Outflow clamp dispatcher for VaultWithdraw and VaultClawback: rounds a
|
||||
* negative delta to the scale appropriate for the Vault's version -- for
|
||||
* FixedPrecision, at AssetsAvailable's own posterior scale (the balance the
|
||||
* cash actually leaves); for Legacy/CashBasis, clampToAssetsTotalScale --
|
||||
* so both call sites share one dispatch point. Same tecPRECISION_LOSS and
|
||||
* integral-asset and magnitude-never-exceeds-delta rules as
|
||||
* clampToAssetsTotalScale.
|
||||
*
|
||||
* @param vault The vault ledger entry.
|
||||
* @param delta The requested signed outflow (negative).
|
||||
* @return The rounded, positive magnitude, or tecPRECISION_LOSS if the
|
||||
* change is below representable precision.
|
||||
*/
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta);
|
||||
|
||||
/**
|
||||
* Returns the Vault's base exponent for asset at scale, before a Vault
|
||||
* object exists to read it from: 0 for an integral asset, -scale otherwise.
|
||||
* Used by VaultCreate::preclaim, which only has the proposed Scale field,
|
||||
* not yet a Vault SLE; getVaultBaseScale is the post-creation equivalent.
|
||||
*
|
||||
* @param asset The (prospective) Vault's underlying asset.
|
||||
* @param scale The (prospective) Vault's sfScale value, ignored for
|
||||
* integral assets.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale);
|
||||
|
||||
/**
|
||||
* Returns true iff value is exactly representable both as an STAmount of
|
||||
* asset and on the base grid at baseScale (no precision lost rounding
|
||||
* TowardsZero at baseScale). Returns false, rather than throwing, for a value
|
||||
* out of range for the asset. Shared representability check for
|
||||
* VaultCreate::preclaim, VaultSet::preclaim and LoanBrokerSet::preclaim's
|
||||
* AssetsMaximum / DebtMaximum / cover-rate field checks.
|
||||
*
|
||||
* @param asset The vault's underlying asset.
|
||||
* @param value The value to check.
|
||||
* @param baseScale The vault's base exponent (see vaultBaseScale /
|
||||
* getVaultBaseScale).
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale);
|
||||
|
||||
/**
|
||||
* Controls whether to truncate shares instead of rounding.
|
||||
*/
|
||||
@@ -360,9 +98,9 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
|
||||
|
||||
/**
|
||||
* Returns true if debiting amount from total (the current value of a
|
||||
* Returns true if debiting `amount` from `total` (the current value of a
|
||||
* vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the
|
||||
* same STAmount value. This happens when amount is non-zero but too small
|
||||
* same STAmount value. This happens when `amount` is non-zero but too small
|
||||
* to change the stored total at STAmount's precision. Shares would still
|
||||
* move, so the ValidVault invariant would fail after apply; callers use
|
||||
* this to reject the transaction upfront instead.
|
||||
@@ -376,20 +114,6 @@ assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive);
|
||||
[[nodiscard]] bool
|
||||
debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount);
|
||||
|
||||
/**
|
||||
* Returns the reference balance a cash outflow's dust check
|
||||
* (debitIsNonZeroDust) should compare against: FixedPrecision Vaults use
|
||||
* AssetsAvailable, the balance the cash actually leaves; Legacy/CashBasis
|
||||
* Vaults use the given AssetsTotal. Shared by VaultWithdraw and
|
||||
* VaultClawback.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param assetsTotal The vault's current AssetsTotal, as already read by the
|
||||
* caller.
|
||||
*/
|
||||
[[nodiscard]] Number
|
||||
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal);
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -434,7 +158,7 @@ sharesToAssetsWithdraw(
|
||||
WaiveUnrealizedLoss waive = WaiveUnrealizedLoss::No);
|
||||
|
||||
/**
|
||||
* Returns true iff account holds all of the vault's outstanding shares,
|
||||
* Returns true iff `account` holds all of the vault's outstanding shares —
|
||||
* i.e. is the sole remaining shareholder. Returns false if the account
|
||||
* holds no shares or fewer than the total outstanding.
|
||||
*
|
||||
@@ -447,13 +171,11 @@ sharesToAssetsWithdraw(
|
||||
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef issuance);
|
||||
|
||||
/**
|
||||
* Resolves a Vault's LEVersion.
|
||||
*
|
||||
* LEVersion is the single point every accounting and rounding helper
|
||||
* should call to decide which protocol a Vault follows. It is written
|
||||
* at VaultCreate and is not updated afterwards, so a Vault created
|
||||
* under an older amendment keeps that behaviour after later amendments
|
||||
* activate. Absent sfLEVersion resolves to VaultVersion::Legacy.
|
||||
* Resolves a Vault's LEVersion, the single point every accounting touch
|
||||
* point should call to determine which recognition model (instant interest
|
||||
* recognition vs. cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1
|
||||
* activated never have sfLEVersion set, which resolves here to
|
||||
* VaultVersion::Legacy.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
*
|
||||
@@ -463,33 +185,6 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault);
|
||||
|
||||
/**
|
||||
* Decodes an already-extracted sfLEVersion value with the same range check as
|
||||
* getVaultVersion. Usable from contexts, such as an invariant's ledger-entry
|
||||
* snapshot, that keep the field value but not the owning SLE.
|
||||
*
|
||||
* @param leVersion The value of sfLEVersion, or nullopt if absent.
|
||||
*
|
||||
* @return The decoded LEVersion, or VaultVersion::Legacy if absent.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion);
|
||||
|
||||
/**
|
||||
* Resolves which LEVersion a newly-created Vault should get under the
|
||||
* currently active amendments. This is the only place that decides Vault
|
||||
* version policy; VaultCreate calls it instead of checking amendments
|
||||
* directly.
|
||||
*
|
||||
* @param rules The active ledger rules.
|
||||
*
|
||||
* @return VaultVersion::FixedPrecision once featureLendingProtocolV1_2,
|
||||
* fixCleanup3_2_0 and fixCleanup3_4_0 are all enabled; VaultVersion::CashBasis
|
||||
* once featureLendingProtocolV1_1 is enabled; VaultVersion::Legacy otherwise.
|
||||
*/
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultDefaultVersion(Rules const& rules);
|
||||
|
||||
/**
|
||||
* Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when
|
||||
* sfVaultKind is present and equal to that value; anything else (including an
|
||||
|
||||
@@ -6,31 +6,21 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* API version numbers used in later API versions
|
||||
* The `api_version` numbers this server serves.
|
||||
*
|
||||
* Requests with a version number in the range
|
||||
* [apiMinimumSupportedVersion, apiMaximumSupportedVersion]
|
||||
* are supported.
|
||||
* A request naming a version in [kApiMinimumSupportedVersion,
|
||||
* kApiMaximumSupportedVersion] is served. With `[beta_rpc_api]` set to `1` in
|
||||
* the config the range extends to kApiBetaVersion.
|
||||
*
|
||||
* If [beta_rpc_api] is enabled in config, the version numbers
|
||||
* in the range [apiMinimumSupportedVersion, apiBetaVersion]
|
||||
* are supported.
|
||||
*
|
||||
* Network Requests without explicit version numbers use
|
||||
* apiVersionIfUnspecified. apiVersionIfUnspecified is 1,
|
||||
* because all the RPC requests with a version >= 2 must
|
||||
* explicitly specify the version in the requests.
|
||||
* Note that apiVersionIfUnspecified will be lower than
|
||||
* apiMinimumSupportedVersion when we stop supporting API
|
||||
* version 1.
|
||||
*
|
||||
* Command line Requests use apiCommandLineVersion.
|
||||
* A request naming no version is served at kApiVersionIfUnspecified, which is 1
|
||||
* because a request wanting any later version states it.
|
||||
*/
|
||||
|
||||
namespace rpc {
|
||||
@@ -57,6 +47,16 @@ static_assert(kApiMaximumSupportedVersion >= kApiMinimumSupportedVersion);
|
||||
static_assert(kApiBetaVersion >= kApiMaximumSupportedVersion);
|
||||
static_assert(kApiMaximumValidVersion >= kApiMaximumSupportedVersion);
|
||||
|
||||
/**
|
||||
* Values accepted in the `ripplerpc` request field, which selects the shape of
|
||||
* the JSON-RPC reply envelope. Distinct from `kJsonRpcVersion` in JsonRpc.h,
|
||||
* which names the JSON-RPC protocol itself, and from the `api_version`
|
||||
* constants above, which select the content of the response.
|
||||
*/
|
||||
inline constexpr std::string_view kRippleRpcVersion1{"1.0"};
|
||||
inline constexpr std::string_view kRippleRpcVersion2{"2.0"};
|
||||
inline constexpr std::string_view kRippleRpcVersion3{"3.0"};
|
||||
|
||||
inline void
|
||||
setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled)
|
||||
{
|
||||
|
||||
@@ -180,11 +180,6 @@ struct ErrorInfo
|
||||
{
|
||||
}
|
||||
|
||||
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message)
|
||||
: code(code), token(token), message(message), httpStatus(200)
|
||||
{
|
||||
}
|
||||
|
||||
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message, int httpStatus)
|
||||
: code(code), token(token), message(message), httpStatus(httpStatus)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
/**
|
||||
* Constants of the JSON-RPC 2.0 protocol itself.
|
||||
*
|
||||
* Kept apart from the `api_version` and `ripplerpc` constants in ApiVersion.h,
|
||||
* which go when support for API versions 1 and 2 goes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Value of the `jsonrpc` member of a request and of its reply, fixed at "2.0"
|
||||
* by the JSON-RPC specification.
|
||||
*/
|
||||
inline constexpr std::string_view kJsonRpcVersion{"2.0"};
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -311,46 +311,24 @@ constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1;
|
||||
* Default IOU scale factor for a Vault
|
||||
*/
|
||||
constexpr std::uint8_t kVaultDefaultIouScale = 6;
|
||||
|
||||
/**
|
||||
* Maximum Scale for a Vault created before featureLendingProtocolV1_2.
|
||||
* Chosen so 1 IOU can always convert to shares:
|
||||
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18.
|
||||
* 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
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumIouScale = 18;
|
||||
|
||||
/**
|
||||
* Maximum Scale for a Vault created under featureLendingProtocolV1_2.
|
||||
*/
|
||||
constexpr std::uint8_t kVaultMaximumFixedPrecisionIouScale = 10;
|
||||
|
||||
/**
|
||||
* Vault ledger-entry schema versions, persisted as sfLEVersion.
|
||||
*
|
||||
* LEVersion records which protocol a Vault was created under so later
|
||||
* amendments can change the rules for new Vaults without rewriting
|
||||
* existing ones. VaultCreate writes it from the then-active lending
|
||||
* amendments; later transactions do not update it. A Vault created
|
||||
* under an older amendment keeps that amendment's behaviour for its
|
||||
* lifetime, even after a newer lending amendment activates.
|
||||
*
|
||||
* Absent sfLEVersion is implicit Legacy (version 0): instant interest
|
||||
* recognition and a dynamic AssetsTotal scale. CashBasis (V1.1) uses
|
||||
* cash-basis recognition on the same dynamic scale. FixedPrecision
|
||||
* (V1.2) keeps cash-basis recognition and adds the lifetime base grid.
|
||||
* Vault ledger-entry schema versions. Assigned to newly created
|
||||
* Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before
|
||||
* activation are left without LEVersion (implicit legacy version 0,
|
||||
* instant interest recognition).
|
||||
*/
|
||||
enum class VaultVersion : uint8_t {
|
||||
Legacy = 0,
|
||||
CashBasis,
|
||||
FixedPrecision,
|
||||
};
|
||||
|
||||
// Code compares VaultVersion values with < and >= (e.g. "CashBasis or later"),
|
||||
// so each later version must stay numerically larger than the one before.
|
||||
static_assert(
|
||||
VaultVersion::Legacy < VaultVersion::CashBasis &&
|
||||
VaultVersion::CashBasis < VaultVersion::FixedPrecision);
|
||||
|
||||
/**
|
||||
* Vault kind. Distinguishes closed-ended vaults from the default open-ended
|
||||
* kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded.
|
||||
|
||||
@@ -766,21 +766,6 @@ roundToAsset(
|
||||
return roundToScale(ret, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an arbitrary precision Number can be converted to an STAmount
|
||||
* of a given Asset without exceeding the asset's range.
|
||||
*
|
||||
* Use this before `roundToAsset` or `STAmount{asset, value}` on unchecked
|
||||
* input, since those throw for out-of-range values, e.g. XRP above
|
||||
* `STAmount::kMaxNativeN`.
|
||||
*
|
||||
* @param asset The relevant asset
|
||||
* @param value The value to be checked
|
||||
* @return true if the value is in range for the asset; false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isRepresentable(Asset const& asset, Number const& value);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
inline bool
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
XRPL_FEATURE(Cosign, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
|
||||
// Requires LendingProtocolV1_1. New vaults take FixedPrecision plus cash-basis.
|
||||
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -514,8 +514,6 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
|
||||
{sfAssetsAvailable, SoeDefault},
|
||||
{sfAssetsMaximum, SoeDefault},
|
||||
{sfLossUnrealized, SoeDefault},
|
||||
{sfYieldUnrealized, SoeDefault},
|
||||
{sfAssetsDeployed, SoeDefault},
|
||||
{sfShareMPTID, SoeRequired},
|
||||
{sfWithdrawalPolicy, SoeRequired},
|
||||
{sfScale, SoeDefault},
|
||||
|
||||
@@ -240,8 +240,6 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
|
||||
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
|
||||
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfYieldUnrealized, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfAssetsDeployed, NUMBER, 19, SField::kSmdNeedsAsset | SField::kSmdDefault)
|
||||
|
||||
// 32-bit signed (common)
|
||||
TYPED_SFIELD(sfLoanScale, INT32, 1)
|
||||
|
||||
@@ -559,7 +559,7 @@ JSS(signing_key); // out: NetworkOPs
|
||||
JSS(signing_keys); // out: ValidatorList
|
||||
JSS(signing_time); // out: NetworkOPs
|
||||
JSS(signer_lists); // in/out: AccountInfo
|
||||
JSS(size); // out: get_aggregate_price
|
||||
JSS(size); // out: get_aggregate_price, ServerHandler
|
||||
JSS(snapshot); // in: Subscribe
|
||||
JSS(source_account); // in: PathRequest, RipplePathFind
|
||||
JSS(source_amount); // in: PathRequest, RipplePathFind
|
||||
|
||||
@@ -242,54 +242,6 @@ public:
|
||||
return this->sle_->isFieldPresent(sfLossUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfYieldUnrealized (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getYieldUnrealized() const
|
||||
{
|
||||
if (hasYieldUnrealized())
|
||||
return this->sle_->at(sfYieldUnrealized);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfYieldUnrealized is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasYieldUnrealized() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfYieldUnrealized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfAssetsDeployed (SoeDefault)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_NUMBER::type::value_type>
|
||||
getAssetsDeployed() const
|
||||
{
|
||||
if (hasAssetsDeployed())
|
||||
return this->sle_->at(sfAssetsDeployed);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfAssetsDeployed is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasAssetsDeployed() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfShareMPTID (SoeRequired)
|
||||
* @return The field value.
|
||||
@@ -619,28 +571,6 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfYieldUnrealized (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setYieldUnrealized(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfYieldUnrealized] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfAssetsDeployed (SoeDefault)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
VaultBuilder&
|
||||
setAssetsDeployed(std::decay_t<typename SF_NUMBER::type::value_type> const& value)
|
||||
{
|
||||
object_[sfAssetsDeployed] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfShareMPTID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -7,6 +7,18 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Writes an HTTP reply carrying @p strMsg with status @p nStatus to @p output,
|
||||
* and logs the status at trace. The body is not logged here: it may carry a
|
||||
* credential this library cannot mask, so the caller logs it masked.
|
||||
*
|
||||
* A 401 with an empty body is answered with the fixed authentication page.
|
||||
*
|
||||
* @param nStatus The HTTP status code.
|
||||
* @param strMsg The body.
|
||||
* @param output Where the reply bytes are written.
|
||||
* @param j The journal the status is logged to.
|
||||
*/
|
||||
void
|
||||
httpReply(int nStatus, std::string const& strMsg, json::Output const&, beast::Journal j);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace json {
|
||||
@@ -241,10 +242,14 @@ Value::Value(xrpl::Number const& value) : type_(ValueType::String), allocated_(t
|
||||
value_.stringVal = valueAllocator()->duplicateStringValue(tmp.c_str(), tmp.length());
|
||||
}
|
||||
|
||||
Value::Value(std::string const& value) : type_(ValueType::String), allocated_(true)
|
||||
Value::Value(std::string const& value) : Value(std::string_view{value})
|
||||
{
|
||||
value_.stringVal =
|
||||
valueAllocator()->duplicateStringValue(value.c_str(), (unsigned int)value.length());
|
||||
}
|
||||
|
||||
Value::Value(std::string_view value) : type_(ValueType::String), allocated_(true)
|
||||
{
|
||||
value_.stringVal = valueAllocator()->duplicateStringValue(
|
||||
value.data(), static_cast<unsigned int>(value.length()));
|
||||
}
|
||||
|
||||
Value::Value(StaticString const& value) : type_(ValueType::String)
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.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/Protocol.h>
|
||||
@@ -21,7 +17,6 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
@@ -29,405 +24,6 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int
|
||||
fixedBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::fixedBaseScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
// sfScale is never written for an integral asset (VaultCreate only sets
|
||||
// it when scale != 0, and integral assets always get scale 0), so it
|
||||
// must not be read here; vaultBaseScale ignores the scale argument for
|
||||
// an integral asset anyway.
|
||||
if (asset.integral())
|
||||
return 0;
|
||||
return vaultBaseScale(asset, vault->at(sfScale));
|
||||
}
|
||||
|
||||
// Thin, vault-specific wrapper over posteriorAssetScale: used by
|
||||
// getPosteriorVaultScale and creditToPosteriorAvailableScale.
|
||||
[[nodiscard]] int
|
||||
posteriorScale(SLE::ConstRef vault, Number const& reference, Number const& delta)
|
||||
{
|
||||
return detail::posteriorAssetScale(
|
||||
getVaultVersion(vault), vault->at(sfAsset), fixedBaseScale(vault), reference, delta);
|
||||
}
|
||||
|
||||
// FixedPrecision-only counterpart to clampToAssetsTotalScale, for cash
|
||||
// outflows (VaultWithdraw, VaultClawback): rounds the magnitude of delta
|
||||
// toward zero at AssetsAvailable's own posterior scale, the balance the cash
|
||||
// actually leaves, instead of the derived AssetsTotal cache. Same
|
||||
// tecPRECISION_LOSS and integral-asset and magnitude-never-exceeds-delta
|
||||
// rules as clampToAssetsTotalScale. Not part of the public interface; reached
|
||||
// only through clampVaultOutflow.
|
||||
//
|
||||
// For an outflow it is safe to round the delta directly, unlike the credit
|
||||
// case: amount on the posterior grid of AssetsAvailable always leaves
|
||||
// AssetsAvailable minus amount representable, since the posterior grid is
|
||||
// exactly the grid AssetsAvailable itself will canonicalize to after the
|
||||
// subtraction.
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampToAvailableScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::clampToAvailableScale : delta and vault asset match");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::clampToAvailableScale : FixedPrecision Vault");
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampToAvailableScale : outflow delta is negative");
|
||||
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount magnitude = delta.negative() ? -delta : delta;
|
||||
if (asset.integral())
|
||||
return magnitude;
|
||||
|
||||
STAmount const rounded = roundToScale(
|
||||
delta,
|
||||
posteriorScale(vault, vault->at(sfAssetsAvailable), delta),
|
||||
Number::RoundingMode::TowardsZero);
|
||||
STAmount actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
|
||||
XRPL_ASSERT(
|
||||
abs(actualDelta) <= abs(delta),
|
||||
"xrpl::clampToAvailableScale : actual delta smaller or equal to calculated delta");
|
||||
|
||||
// Reject changes below scale precision (1 ULP) to prevent share balance changes
|
||||
// without corresponding asset movements.
|
||||
if (actualDelta <= beast::kZero)
|
||||
return std::unexpected(tecPRECISION_LOSS);
|
||||
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
// Write cached AssetsTotal from getAssetsTotal, as the exact total rounded
|
||||
// Downward to the Vault asset's 16-digit STAmount representation. Below
|
||||
// coarsening this equals the exact total; above it, the cache is a
|
||||
// deliberate floor of the exact value. Internal to adjustVaultBalances,
|
||||
// the only writer of AssetsAvailable, AssetsDeployed, AssetsTotal,
|
||||
// YieldUnrealized and LossUnrealized on a FixedPrecision Vault after
|
||||
// creation; not part of the public interface.
|
||||
void
|
||||
syncAssetsTotal(SLE::Ref vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::syncAssetsTotal : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
|
||||
// adjustVaultBalances writes AssetsAvailable as an already-canonical
|
||||
// STAmount before calling this, so the associateAsset pass a transactor
|
||||
// runs afterwards is always a no-op. If this fires, AssetsAvailable was
|
||||
// written some other way.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, vault->at(sfAssetsAvailable)} == vault->at(sfAssetsAvailable)),
|
||||
"xrpl::syncAssetsTotal : AssetsAvailable is a 16-digit STAmount value");
|
||||
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
vault->at(sfAssetsTotal) = STAmount{asset, getAssetsTotal(vault)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace detail {
|
||||
|
||||
[[nodiscard]] int
|
||||
liveScale(Number const& reference, Asset const& asset, int baseScale)
|
||||
{
|
||||
if (reference == beast::kZero)
|
||||
return baseScale;
|
||||
// Round Downward, not the ambient mode: the live exponent must match the
|
||||
// exponent syncAssetsTotal actually stores (also Downward), regardless of
|
||||
// what rounding mode the caller's own arithmetic is using.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return std::max(baseScale, scale(reference, asset));
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
posteriorAssetScale(
|
||||
VaultVersion version,
|
||||
Asset const& asset,
|
||||
int baseScale,
|
||||
Number const& reference,
|
||||
Number const& delta)
|
||||
{
|
||||
// ToNearest for the sum and for scale()'s canonicalization of it, as the
|
||||
// Legacy/CashBasis clamp always did; liveScale sets its own mode.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
Number const posterior = reference + delta;
|
||||
|
||||
switch (version)
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(posterior, asset);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return liveScale(posterior, asset, baseScale);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::detail::posteriorAssetScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorScale(
|
||||
Asset const& asset,
|
||||
Number const& reference,
|
||||
int atScale,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
// Floor the SUM (reference + raw), not just raw, at atScale. See
|
||||
// creditToPosteriorAvailableScale's doc: a delta floored on its own grid
|
||||
// can still leave a 17-digit sum once the reference has crossed a power
|
||||
// of ten.
|
||||
//
|
||||
// Both reference + raw and flooredSum - reference can carry more digits
|
||||
// than Number holds (the sum is on the posterior grid, the reference on
|
||||
// the finer one). Compute both under roundingMode, not the caller's
|
||||
// ambient mode, so the credit this returns never exceeds raw.
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
Number const flooredSum = roundToAsset(asset, reference + raw, atScale, roundingMode);
|
||||
return STAmount{asset, flooredSum - reference};
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getPosteriorVaultScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::detail::getPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
delta.asset() == vault->at(sfAsset),
|
||||
"xrpl::detail::getPosteriorVaultScale : delta and Vault asset match");
|
||||
return posteriorScale(vault, getAssetsTotal(vault), delta);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
[[nodiscard]] Number
|
||||
getAssetsTotal(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getAssetsTotal : valid Vault sle");
|
||||
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return vault->at(sfAssetsTotal);
|
||||
|
||||
// AssetsAvailable is at -Scale (or coarser) and AssetsDeployed is always at
|
||||
// -Scale, so the sum is exact while it fits 19 digits. Fix the mode so a
|
||||
// sum that does not fit rounds the same way for every caller.
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return vault->at(sfAssetsAvailable) + vault->at(sfAssetsDeployed);
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
adjustVaultBalances(SLE::Ref vault, VaultBalanceChange const& change, beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::adjustVaultBalances : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::adjustVaultBalances : FixedPrecision Vault");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
STAmount const cash = change.cash.value_or(STAmount{asset, 0});
|
||||
XRPL_ASSERT(cash.asset() == asset, "xrpl::adjustVaultBalances : cash and Vault asset match");
|
||||
// This is the mode the ledger's own transfers (trust line / MPT balance
|
||||
// updates) normalize in; computing AA' with any other ambient mode would
|
||||
// not reproduce what the real transfer just did to the backing balance.
|
||||
XRPL_ASSERT(
|
||||
Number::getround() == Number::RoundingMode::ToNearest,
|
||||
"xrpl::adjustVaultBalances : ambient rounding mode is ToNearest");
|
||||
|
||||
// AA' is the exact trust-line/MPT arithmetic the ledger uses for the
|
||||
// transfer itself: a Number sum re-canonicalized to the asset's 16-digit
|
||||
// STAmount precision. MPT and XRP sums are already integral, so this is
|
||||
// a no-op rounding for them.
|
||||
//
|
||||
// AssetsDeployed, LossUnrealized and YieldUnrealized move no cash, so they stay
|
||||
// exact Number sums; the asserts below check each fits 16 digits, rather
|
||||
// than letting an STAmount conversion round it silently.
|
||||
STAmount const availableAfter{asset, Number(vault->at(sfAssetsAvailable)) + Number(cash)};
|
||||
Number const assetsDeployedAfter = Number(vault->at(sfAssetsDeployed)) + change.deployed;
|
||||
Number const lossUnrealizedAfter = Number(vault->at(sfLossUnrealized)) + change.loss;
|
||||
// LoanSet, LoanPay and default each move YieldUnrealized by the change in
|
||||
// the Loan's own TotalValueOutstanding - PrincipalOutstanding -
|
||||
// ManagementFeeOutstanding, so it always equals that sum over the
|
||||
// Vault's Loans and cannot go negative.
|
||||
Number const yieldUnrealizedAfter = Number(vault->at(sfYieldUnrealized)) + change.yield;
|
||||
|
||||
if (availableAfter < beast::kZero || assetsDeployedAfter < beast::kZero ||
|
||||
lossUnrealizedAfter < beast::kZero || yieldUnrealizedAfter < beast::kZero)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "adjustVaultBalances: a balance would become negative."
|
||||
<< " AssetsAvailable: " << Number(availableAfter)
|
||||
<< ", AssetsDeployed: " << assetsDeployedAfter
|
||||
<< ", LossUnrealized: " << lossUnrealizedAfter
|
||||
<< ", YieldUnrealized: " << yieldUnrealizedAfter;
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// The impairment rule: a Loan can only be impaired for as much as it
|
||||
// still owes. Reachable only through LoanManage's impair.
|
||||
if (lossUnrealizedAfter > assetsDeployedAfter)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
// Every caller derives principal from a Loan's own PrincipalOutstanding so the new total must
|
||||
// stay representable at the asset's own 16-digit precision too.
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, assetsDeployedAfter} == assetsDeployedAfter),
|
||||
"xrpl::adjustVaultBalances : AssetsDeployed is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, lossUnrealizedAfter} == lossUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : LossUnrealized is a 16-digit STAmount value");
|
||||
XRPL_ASSERT(
|
||||
(STAmount{asset, yieldUnrealizedAfter} == yieldUnrealizedAfter),
|
||||
"xrpl::adjustVaultBalances : YieldUnrealized is a 16-digit STAmount value");
|
||||
|
||||
vault->at(sfAssetsAvailable) = availableAfter;
|
||||
vault->at(sfAssetsDeployed) = assetsDeployedAfter;
|
||||
vault->at(sfLossUnrealized) = lossUnrealizedAfter;
|
||||
vault->at(sfYieldUnrealized) = yieldUnrealizedAfter;
|
||||
syncAssetsTotal(vault);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultScale : valid Vault sle");
|
||||
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return scale(assetsTotal, vault->at(sfAsset));
|
||||
case VaultVersion::FixedPrecision:
|
||||
return detail::liveScale(assetsTotal, vault->at(sfAsset), fixedBaseScale(vault));
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
getVaultBaseScale(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultBaseScale : valid Vault sle");
|
||||
|
||||
switch (getVaultVersion(vault))
|
||||
{
|
||||
case VaultVersion::Legacy:
|
||||
case VaultVersion::CashBasis:
|
||||
return getVaultScale(vault);
|
||||
case VaultVersion::FixedPrecision:
|
||||
return fixedBaseScale(vault);
|
||||
}
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultBaseScale : valid VaultVersion");
|
||||
return Number::kMinExponent - 1;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
roundToPosteriorVaultScale(
|
||||
SLE::ConstRef vault,
|
||||
STAmount const& amount,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::roundToPosteriorVaultScale : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::roundToPosteriorVaultScale : amount and Vault asset match");
|
||||
if (amount.integral())
|
||||
return amount;
|
||||
return roundToScale(amount, detail::getPosteriorVaultScale(vault, amount), roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
creditToPosteriorAvailableScale(
|
||||
SLE::ConstRef vault,
|
||||
Number const& raw,
|
||||
Number::RoundingMode roundingMode)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT,
|
||||
"xrpl::creditToPosteriorAvailableScale : valid Vault sle");
|
||||
Asset const asset = vault->at(sfAsset);
|
||||
if (asset.integral())
|
||||
{
|
||||
NumberRoundModeGuard const rg(roundingMode);
|
||||
return STAmount{asset, raw};
|
||||
}
|
||||
|
||||
Number const reference = vault->at(sfAssetsAvailable);
|
||||
int const scale = posteriorScale(vault, reference, raw);
|
||||
return detail::creditToPosteriorScale(asset, reference, scale, raw, roundingMode);
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
getVaultOpenLimit(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultOpenLimit : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::getVaultOpenLimit : FixedPrecision Vault");
|
||||
return Number{9, 15 + getVaultBaseScale(vault)};
|
||||
}
|
||||
|
||||
[[nodiscard]] TER
|
||||
checkOptionalVaultInflow(SLE::ConstRef vault, STAmount const& amount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::checkOptionalVaultInflow : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == vault->at(sfAsset),
|
||||
"xrpl::checkOptionalVaultInflow : amount and Vault asset match");
|
||||
XRPL_ASSERT(!amount.negative(), "xrpl::checkOptionalVaultInflow : non-negative amount");
|
||||
if (amount.negative())
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
if (getVaultVersion(vault) != VaultVersion::FixedPrecision)
|
||||
return tesSUCCESS;
|
||||
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, amount, Number::RoundingMode::TowardsZero);
|
||||
int const baseScale = getVaultBaseScale(vault);
|
||||
// Keep this explicit even though a non-negative YieldUnrealized makes the
|
||||
// Open-zone capacity ceiling reject every coarsening transition too. The
|
||||
// protocol defines both conditions independently.
|
||||
if (detail::getPosteriorVaultScale(vault, rounded) != baseScale)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
|
||||
if (vaultOpenZoneCapacity(vault, rounded) > getVaultOpenLimit(vault))
|
||||
return tecLIMIT_EXCEEDED;
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
vaultOpenZoneCapacity(SLE::ConstRef vault, Number const& roundedAmount)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
vault && vault->getType() == ltVAULT, "xrpl::vaultOpenZoneCapacity : valid Vault sle");
|
||||
XRPL_ASSERT(
|
||||
getVaultVersion(vault) == VaultVersion::FixedPrecision,
|
||||
"xrpl::vaultOpenZoneCapacity : FixedPrecision Vault");
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::TowardsZero);
|
||||
return getAssetsTotal(vault) + vault->at(sfYieldUnrealized) + roundedAmount;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount const& assets)
|
||||
{
|
||||
@@ -438,7 +34,7 @@ assetsToSharesDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (assets.negative() || assets.asset() != vault->at(sfAsset))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
STAmount shares{vault->at(sfShareMPTID)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -462,7 +58,7 @@ sharesToAssetsDeposit(SLE::ConstRef vault, SLE::ConstRef issuance, STAmount cons
|
||||
if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Number const assetTotal = getAssetsTotal(vault);
|
||||
Number const assetTotal = vault->at(sfAssetsTotal);
|
||||
STAmount assets{vault->at(sfAsset)};
|
||||
if (assetTotal == 0)
|
||||
{
|
||||
@@ -489,52 +85,37 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
return magnitude;
|
||||
}
|
||||
Number const assetsTotal = vault->at(sfAssetsTotal);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
}();
|
||||
|
||||
STAmount actualDelta;
|
||||
if (getVaultVersion(vault) == VaultVersion::FixedPrecision)
|
||||
if (delta.negative())
|
||||
{
|
||||
// FixedPrecision only reaches this branch for credits (VaultDeposit);
|
||||
// outflows go through clampVaultOutflow -> clampToAvailableScale,
|
||||
// which rounds at AssetsAvailable's own posterior scale instead.
|
||||
XRPL_ASSERT(
|
||||
!delta.negative(), "xrpl::clampToAssetsTotalScale : FixedPrecision credit only");
|
||||
STAmount const rounded =
|
||||
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero);
|
||||
actualDelta = rounded.negative() ? -rounded : rounded;
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
else
|
||||
{
|
||||
Number const assetsTotal = getAssetsTotal(vault);
|
||||
|
||||
// Calculate the scale after applying the delta using ToNearest rounding.
|
||||
// This aligns the delta with scale checks used by vault invariants.
|
||||
int const postScale = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(assetsTotal + delta, asset);
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
}();
|
||||
|
||||
if (delta.negative())
|
||||
{
|
||||
// For withdrawals (debits), floor the magnitude to the target scale
|
||||
// to ensure exact grid alignment without paying out extra assets.
|
||||
actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For deposits (credits), derive actualDelta from the floored posterior total.
|
||||
// This prevents grid alignment issues from crediting the vault more than deposited.
|
||||
//
|
||||
// Sum using Downward rounding so intermediate precision doesn't round up
|
||||
// and exceed the original requested amount.
|
||||
Number const posterior = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return assetsTotal + magnitude;
|
||||
}();
|
||||
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
}
|
||||
Number const roundedPosterior =
|
||||
roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
|
||||
actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
@@ -549,36 +130,10 @@ clampToAssetsTotalScale(SLE::ConstRef vault, STAmount const& delta)
|
||||
return actualDelta;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::expected<STAmount, TER>
|
||||
clampVaultOutflow(SLE::ConstRef vault, STAmount const& delta)
|
||||
{
|
||||
XRPL_ASSERT(delta.negative(), "xrpl::clampVaultOutflow : outflow delta is negative");
|
||||
return getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? clampToAvailableScale(vault, delta)
|
||||
: clampToAssetsTotalScale(vault, delta);
|
||||
}
|
||||
|
||||
[[nodiscard]] int
|
||||
vaultBaseScale(Asset const& asset, std::uint8_t scale)
|
||||
{
|
||||
if (asset.integral())
|
||||
return 0;
|
||||
return -static_cast<int>(scale);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isOnVaultBaseGrid(Asset const& asset, Number const& value, int baseScale)
|
||||
{
|
||||
// isRepresentable first: building an STAmount from an out-of-range value
|
||||
// throws, and this checks user-supplied values in preclaim.
|
||||
return isRepresentable(asset, value) && STAmount{asset, value} == value &&
|
||||
roundToAsset(asset, value, baseScale, Number::RoundingMode::TowardsZero) == value;
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
assetsTotalForWithdrawal(SLE::ConstRef vault, WaiveUnrealizedLoss waive)
|
||||
{
|
||||
Number assetTotal = getAssetsTotal(vault);
|
||||
Number assetTotal = vault->at(sfAssetsTotal);
|
||||
if (waive == WaiveUnrealizedLoss::No)
|
||||
assetTotal -= vault->at(sfLossUnrealized);
|
||||
return assetTotal;
|
||||
@@ -592,14 +147,6 @@ debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount
|
||||
return STAmount{asset, total - amount} == STAmount{asset, total};
|
||||
}
|
||||
|
||||
[[nodiscard]] Number
|
||||
vaultDebitDustReference(SLE::ConstRef vault, Number const& assetsTotal)
|
||||
{
|
||||
return getVaultVersion(vault) == VaultVersion::FixedPrecision
|
||||
? Number(vault->at(sfAssetsAvailable))
|
||||
: assetsTotal;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesWithdraw(
|
||||
SLE::ConstRef vault,
|
||||
@@ -670,41 +217,36 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::ConstRef
|
||||
return sleToken->getFieldU64(sfMPTAmount) == outstanding;
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
decodeVaultVersion(std::optional<std::uint8_t> leVersion)
|
||||
{
|
||||
if (!leVersion)
|
||||
return VaultVersion::Legacy;
|
||||
if (*leVersion > std::to_underlying(VaultVersion::FixedPrecision))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::decodeVaultVersion : invalid vault version");
|
||||
return VaultVersion::Legacy;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return static_cast<VaultVersion>(*leVersion);
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::ConstRef vault)
|
||||
{
|
||||
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle");
|
||||
return decodeVaultVersion(vault->at(~sfLEVersion));
|
||||
if (!vault->isFieldPresent(sfLEVersion))
|
||||
return VaultVersion::Legacy;
|
||||
|
||||
auto const version = vault->at(sfLEVersion);
|
||||
if (version > std::to_underlying(VaultVersion::CashBasis))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("xrpl::getVaultVersion : invalid vault version");
|
||||
return VaultVersion::Legacy;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
return static_cast<VaultVersion>(version);
|
||||
}
|
||||
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultDefaultVersion(Rules const& rules)
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
decodeVaultKind(std::optional<std::uint8_t> vaultKind)
|
||||
{
|
||||
// FixedPrecision requires both cleanups, so a FixedPrecision Vault always
|
||||
// sees them enabled.
|
||||
if (rules.enabled(featureLendingProtocolV1_2) && rules.enabled(fixCleanup3_2_0) &&
|
||||
rules.enabled(fixCleanup3_4_0))
|
||||
return VaultVersion::FixedPrecision;
|
||||
if (rules.enabled(featureLendingProtocolV1_1))
|
||||
return VaultVersion::CashBasis;
|
||||
return VaultVersion::Legacy;
|
||||
if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
|
||||
return VaultKind::ClosedEnded;
|
||||
return VaultKind::OpenEnded;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] VaultKind
|
||||
getVaultKind(SLE::ConstRef vault)
|
||||
{
|
||||
|
||||
@@ -20,88 +20,92 @@ namespace detail {
|
||||
// will remain in the object file. But the string literals will remain.
|
||||
//
|
||||
// There's a certain amount of tension in determining the correct HTTP
|
||||
// status to associate with a given RPC error. Initially all RPC errors
|
||||
// returned 200 (OK). And that's the default behavior if no HTTP status code
|
||||
// is specified below.
|
||||
// status to associate with a given RPC error. Every row below names its
|
||||
// HTTP status, and a row that omits it does not compile, since ErrorInfo has
|
||||
// no constructor that leaves the status out.
|
||||
//
|
||||
// The codes currently selected target the load balancer fail-over use case.
|
||||
// If a query fails on one node but is likely to have a positive outcome
|
||||
// on a different node, then the failure should return a 4xx/5xx range
|
||||
// status code.
|
||||
|
||||
// The rows below are aligned by hand into four columns, so that a wrong status or a mistyped token
|
||||
// is visible by scanning one column. That cannot be had inside the 100-column limit once the
|
||||
// longest enumerator and the longest message sit in one row. So this region is exempt from the
|
||||
// limit by design rather than by oversight.
|
||||
// clang-format off
|
||||
static constexpr ErrorInfo kUnorderedErrorInfos[]{
|
||||
{RpcActMalformed, "actMalformed", "Account malformed."},
|
||||
{RpcActNotFound, "actNotFound", "Account not found."},
|
||||
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned."},
|
||||
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed."},
|
||||
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
|
||||
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
|
||||
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
|
||||
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
|
||||
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
|
||||
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
|
||||
{RpcBadMarket, "badMarket", "No such market.", 404},
|
||||
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
|
||||
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
|
||||
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
|
||||
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
|
||||
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
|
||||
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
|
||||
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
|
||||
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
|
||||
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
|
||||
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
|
||||
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
|
||||
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
|
||||
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
|
||||
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
|
||||
{RpcForbidden, "forbidden", "Bad credentials.", 403},
|
||||
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
|
||||
{RpcInternal, "internal", "Internal error.", 500},
|
||||
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
|
||||
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
|
||||
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
|
||||
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
|
||||
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
|
||||
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
|
||||
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
|
||||
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
|
||||
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
|
||||
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
|
||||
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
|
||||
{RpcNotImpl, "notImpl", "Not implemented.", 501},
|
||||
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
|
||||
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
|
||||
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
|
||||
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
|
||||
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
|
||||
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
|
||||
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
|
||||
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
|
||||
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
|
||||
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
|
||||
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
|
||||
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
|
||||
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
|
||||
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
|
||||
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
|
||||
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
|
||||
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
|
||||
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
|
||||
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
|
||||
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
|
||||
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
|
||||
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
|
||||
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
|
||||
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
|
||||
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
|
||||
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
|
||||
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
|
||||
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
|
||||
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
|
||||
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
|
||||
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
|
||||
{RpcActMalformed, "actMalformed", "Account malformed.", 400},
|
||||
{RpcActNotFound, "actNotFound", "Account not found.", 404},
|
||||
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned.", 400},
|
||||
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed.", 400},
|
||||
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
|
||||
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
|
||||
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
|
||||
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
|
||||
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
|
||||
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
|
||||
{RpcBadMarket, "badMarket", "No such market.", 404},
|
||||
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
|
||||
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
|
||||
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
|
||||
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
|
||||
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
|
||||
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
|
||||
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
|
||||
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
|
||||
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
|
||||
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
|
||||
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
|
||||
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
|
||||
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
|
||||
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
|
||||
{RpcForbidden, "forbidden", "Bad credentials.", 403},
|
||||
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
|
||||
{RpcInternal, "internal", "Internal error.", 500},
|
||||
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
|
||||
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
|
||||
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
|
||||
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
|
||||
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
|
||||
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
|
||||
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
|
||||
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
|
||||
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
|
||||
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
|
||||
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
|
||||
{RpcNotImpl, "notImpl", "Not implemented.", 501},
|
||||
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
|
||||
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
|
||||
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
|
||||
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
|
||||
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
|
||||
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
|
||||
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
|
||||
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
|
||||
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
|
||||
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
|
||||
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
|
||||
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
|
||||
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
|
||||
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
|
||||
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
|
||||
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
|
||||
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
|
||||
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
|
||||
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
|
||||
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
|
||||
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
|
||||
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
|
||||
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
|
||||
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
|
||||
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
|
||||
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
|
||||
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
|
||||
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
|
||||
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
|
||||
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
|
||||
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -443,22 +443,6 @@ getRate(STAmount const& offerOut, STAmount const& offerIn)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
isRepresentable(Asset const& asset, Number const& value)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Out-of-range values throw std::overflow_error from Number or
|
||||
// IOUAmount, or std::runtime_error from STAmount::canonicalize.
|
||||
[[maybe_unused]] STAmount const amount{asset, value};
|
||||
return true;
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Safely checks if two STAmount values can be added without overflow,
|
||||
* underflow, or precision loss.
|
||||
|
||||
@@ -33,7 +33,10 @@ getHTTPHeaderTimestamp()
|
||||
void
|
||||
httpReply(int nStatus, std::string const& content, json::Output const& output, beast::Journal j)
|
||||
{
|
||||
JLOG(j.trace()) << "HTTP Reply " << nStatus << " " << content;
|
||||
// The status only. The body is a reply, which carries a credential when the
|
||||
// command is a keygen, and this library cannot mask one: the caller logs the
|
||||
// masked body at debug.
|
||||
JLOG(j.trace()) << "HTTP Reply " << nStatus;
|
||||
|
||||
if (content.empty() && nStatus == 401)
|
||||
{
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
@@ -224,15 +223,10 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
|
||||
}
|
||||
|
||||
// Check that relevant values can be represented as the vault asset
|
||||
// type. This is mostly only relevant for integral (non-IOU) types.
|
||||
// fixCleanup3_5_0 also rejects out-of-range values, e.g. XRP above
|
||||
// STAmount::kMaxNativeN, which would otherwise throw.
|
||||
bool const fix350Enabled = ctx.view.rules().enabled(fixCleanup3_5_0);
|
||||
// type. This is mostly only relevant for integral (non-IOU) types
|
||||
for (auto const& field : getValueFields())
|
||||
{
|
||||
if (auto const value = tx[field]; value &&
|
||||
((fix350Enabled && !isRepresentable(asset, *value)) ||
|
||||
STAmount{asset, *value} != *value))
|
||||
if (auto const value = tx[field]; value && STAmount{asset, *value} != *value)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
|
||||
<< ") can not be represented as a(n) " << to_string(asset) << ".";
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
@@ -367,14 +366,10 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
// Check that relevant values can be represented as the vault asset type.
|
||||
// This check is almost duplicated in doApply, but that check is done after
|
||||
// the overall loan scale is known. This is mostly only relevant for
|
||||
// integral (non-IOU) types. fixCleanup3_5_0 also rejects out-of-range
|
||||
// values, e.g. XRP above STAmount::kMaxNativeN, which would otherwise throw.
|
||||
bool const fix350Enabled = ctx.view.rules().enabled(fixCleanup3_5_0);
|
||||
// integral (non-IOU) types
|
||||
for (auto const& field : getValueFields())
|
||||
{
|
||||
if (auto const value = tx[field]; value &&
|
||||
((fix350Enabled && !isRepresentable(asset, *value)) ||
|
||||
STAmount{asset, *value} != *value))
|
||||
if (auto const value = tx[field]; value && STAmount{asset, *value} != *value)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
|
||||
<< ") can not be represented as a(n) " << to_string(asset) << ".";
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.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>
|
||||
@@ -88,11 +87,6 @@ VaultCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (*assetMax < beast::kZero)
|
||||
return temMALFORMED;
|
||||
|
||||
// An AssetsMaximum out of range for the asset, e.g. XRP above
|
||||
// STAmount::kMaxNativeN, would throw when rounded to the asset.
|
||||
if (ctx.rules.enabled(fixCleanup3_5_0) && !isRepresentable(ctx.tx[sfAsset], *assetMax))
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (auto const metadata = ctx.tx[~sfMPTokenMetadata])
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.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>
|
||||
@@ -75,16 +74,6 @@ VaultSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// An AssetsMaximum out of range for the vault asset, e.g. XRP above
|
||||
// STAmount::kMaxNativeN, would throw when rounded to the asset.
|
||||
if (auto const assetMax = ctx.tx[~sfAssetsMaximum]; assetMax &&
|
||||
ctx.view.rules().enabled(fixCleanup3_5_0) &&
|
||||
!isRepresentable(vault->at(sfAsset), *assetMax))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultSet: max assets out of range for the asset.";
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
auto const mptIssuanceID = (*vault)[sfShareMPTID];
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
|
||||
@@ -2419,8 +2419,7 @@ public:
|
||||
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jrr));
|
||||
BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -2170,8 +2170,7 @@ public:
|
||||
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jrr));
|
||||
BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -3369,42 +3369,6 @@ class LoanBroker_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testDebtMaximumOutOfRange(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace loan_broker;
|
||||
|
||||
bool const withFix = features[fixCleanup3_5_0];
|
||||
testcase(
|
||||
std::string("DebtMaximum out of range for the asset (") + (withFix ? "post" : "pre") +
|
||||
"-fixCleanup3_5_0)");
|
||||
|
||||
Env env(*this, features);
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(100'000), alice);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
auto const [tx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
// Without the fix, the representability check in preclaim throws for
|
||||
// XRP above STAmount::kMaxNativeN.
|
||||
auto const ownerCount = env.ownerCount(alice);
|
||||
Number const maxXrp{static_cast<std::int64_t>(STAmount::kMaxNativeN)};
|
||||
env(set(alice, keylet.key),
|
||||
kDebtMaximum(maxXrp + 1),
|
||||
Ter(withFix ? TER{tecPRECISION_LOSS} : TER{tefEXCEPTION}));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) == ownerCount);
|
||||
|
||||
env(set(alice, keylet.key), kDebtMaximum(maxXrp));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(alice) > ownerCount);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -3445,9 +3409,6 @@ public:
|
||||
testLoanBrokerDeleteConfidentialCOA(all_);
|
||||
testLoanBrokerDeleteConfidentialCOA(all_ - fixCleanup3_5_0);
|
||||
|
||||
testDebtMaximumOutOfRange(all_);
|
||||
testDebtMaximumOutOfRange(all_ - fixCleanup3_5_0);
|
||||
|
||||
// featureMPTokensV2 independently makes ValidMPTTransfer enforcing,
|
||||
// but it's Supported::No (never enabled on real networks); exclude
|
||||
// it here so fixCleanup3_4_0 alone is the deciding amendment, as it
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -933,38 +932,6 @@ private:
|
||||
run(all_, tesSUCCESS);
|
||||
}
|
||||
|
||||
void
|
||||
testPrincipalRequestedOutOfRange(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace loan;
|
||||
|
||||
bool const withFix = features[fixCleanup3_5_0];
|
||||
testcase(
|
||||
std::string("LoanSet PrincipalRequested out of range for the asset (") +
|
||||
(withFix ? "post" : "pre") + "-fixCleanup3_5_0)");
|
||||
|
||||
Env env(*this, features);
|
||||
Account const lender{"lender"};
|
||||
Account const borrower{"borrower"};
|
||||
env.fund(XRP(10'000'000), lender, borrower);
|
||||
env.close();
|
||||
|
||||
PrettyAsset const xrp{xrpIssue(), 1'000'000};
|
||||
auto const broker = createVaultAndBroker(env, xrp, lender);
|
||||
|
||||
// Without the fix, the representability check in preclaim throws for
|
||||
// XRP above STAmount::kMaxNativeN.
|
||||
auto const ownerCount = env.ownerCount(borrower);
|
||||
Number const maxXrp{static_cast<std::int64_t>(STAmount::kMaxNativeN)};
|
||||
env(set(borrower, broker.brokerID, maxXrp + 1),
|
||||
Sig(sfCounterpartySignature, lender),
|
||||
Fee(env.current()->fees().base * 2),
|
||||
Ter(withFix ? TER{tecPRECISION_LOSS} : TER{tefEXCEPTION}));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(borrower) == ownerCount);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -977,9 +944,6 @@ public:
|
||||
testLoanSetExistingLineAfterIssuerClearsDefaultRipple();
|
||||
testLoanSetOriginationFeeTwoMptCreates(all_);
|
||||
testLoanSetOriginationFeeTwoMptCreates(all_ - fixCleanup3_4_0);
|
||||
|
||||
testPrincipalRequestedOutOfRange(all_);
|
||||
testPrincipalRequestedOutOfRange(all_ - fixCleanup3_5_0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STIssue.h>
|
||||
@@ -21,19 +20,17 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// True unit test of clampToAssetsTotalScale. The function under test reads
|
||||
// only fields from the vault SLE and never touches a ledger view or Rules, so
|
||||
// a bare in-memory ltVAULT SLE is enough; there is no jtx::Env and no
|
||||
// transaction submitted anywhere in this file.
|
||||
// True unit test of `clampToAssetsTotalScale`. The function under test only
|
||||
// reads sfAsset and sfAssetsTotal from the vault SLE and never touches a
|
||||
// ledger view or Rules, so a bare in-memory ltVAULT SLE is enough; there is
|
||||
// no jtx::Env and no transaction submitted anywhere in this file.
|
||||
//
|
||||
// Number regime: this suite relies on the default thread_local Number
|
||||
// mantissa range, which src/libxrpl/basics/Number.cpp initializes to
|
||||
@@ -42,17 +39,17 @@ namespace xrpl {
|
||||
// thread_local std::reference_wrapper<MantissaRange const> Number::kRange =
|
||||
// MantissaRange::Access::mantissaRange(MantissaRange::MantissaScale::Large330);
|
||||
//
|
||||
// Unlike transaction processing, this test never constructs a ledger Rules
|
||||
// object, so STAmount::operator=(Number const&) always takes its
|
||||
// !getCurrentTransactionRules() branch and calls fromNumber, independent
|
||||
// Unlike transaction processing, this test never constructs a ledger `Rules`
|
||||
// object, so `STAmount::operator=(Number const&)` always takes its
|
||||
// `!getCurrentTransactionRules()` branch and calls `fromNumber`, independent
|
||||
// of amendment state. testProbeLarge330Regime() below asserts directly on a
|
||||
// value that only round-trips exactly under Large330, pinning the regime
|
||||
// rather than merely asserting it by comment.
|
||||
class VaultHelpers_test : public beast::unit_test::Suite
|
||||
{
|
||||
private:
|
||||
// A single row of the clampToAssetsTotalScale table. assetsTotal and
|
||||
// delta must already be genuine, on-grid STAmount values for asset.
|
||||
// A single row of the clampToAssetsTotalScale table. `assetsTotal` and
|
||||
// `delta` must already be genuine, on-grid STAmount values for `asset`.
|
||||
struct Case
|
||||
{
|
||||
char const* name = nullptr;
|
||||
@@ -61,44 +58,27 @@ private:
|
||||
std::optional<Number> expected; // nullopt means tecPRECISION_LOSS
|
||||
};
|
||||
|
||||
// Builds a bare ltVAULT SLE. With fixedScale absent, sfLEVersion stays
|
||||
// absent too, preserving the pre-V1.2 Legacy behavior exercised by the
|
||||
// existing clamp table. With fixedScale set, the SLE is stamped
|
||||
// FixedPrecision with that Scale, exercising clampToAssetsTotalScale's
|
||||
// roundToPosteriorVaultScale branch instead.
|
||||
// Builds a bare ltVAULT SLE with only sfAsset and sfAssetsTotal set,
|
||||
// mirroring what a transactor does: set the STNumber field, then call
|
||||
// associateAsset() so it is quantized to the asset's STAmount grid, the
|
||||
// same way VaultDeposit::doApply does for a real vault (see
|
||||
// src/libxrpl/tx/transactors/vault/VaultDeposit.cpp).
|
||||
static std::shared_ptr<SLE>
|
||||
makeVault(
|
||||
Asset const& asset,
|
||||
Number const& assetsTotal,
|
||||
std::optional<std::uint8_t> fixedScale = std::nullopt)
|
||||
makeVault(Asset const& asset, Number const& assetsTotal)
|
||||
{
|
||||
auto vault = std::make_shared<SLE>(keylet::vault(UInt256(1)));
|
||||
vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
|
||||
vault->at(sfAssetsTotal) = assetsTotal;
|
||||
associateAsset(*vault, asset);
|
||||
if (fixedScale)
|
||||
{
|
||||
vault->at(sfLEVersion) = std::to_underlying(VaultVersion::FixedPrecision);
|
||||
vault->at(sfScale) = *fixedScale;
|
||||
// FixedPrecision derives its total from these two fields.
|
||||
vault->at(sfAssetsAvailable) = assetsTotal;
|
||||
vault->at(sfAssetsDeployed) = Number{0};
|
||||
}
|
||||
return vault;
|
||||
}
|
||||
|
||||
// Runs every case in cases against asset, once per ambient rounding
|
||||
// Runs every case in `cases` against `asset`, once per ambient rounding
|
||||
// mode. The function must give the same answer under all four modes,
|
||||
// and its answer must match the hand-derived expected value.
|
||||
// fixedScale, when set, builds a FixedPrecision vault at that Scale
|
||||
// instead of the default Legacy vault.
|
||||
// and its answer must match the hand-derived `expected` value.
|
||||
template <std::size_t N>
|
||||
void
|
||||
runCases(
|
||||
Asset const& asset,
|
||||
std::array<Case, N> const& cases,
|
||||
std::optional<std::uint8_t> fixedScale = std::nullopt,
|
||||
bool viaClampVaultOutflow = false)
|
||||
runCases(Asset const& asset, std::array<Case, N> const& cases)
|
||||
{
|
||||
std::array<Number::RoundingMode, 4> const modes{
|
||||
Number::RoundingMode::ToNearest,
|
||||
@@ -111,7 +91,7 @@ private:
|
||||
{
|
||||
testcase(c.name);
|
||||
|
||||
auto const vault = makeVault(asset, c.assetsTotal, fixedScale);
|
||||
auto const vault = makeVault(asset, c.assetsTotal);
|
||||
BEAST_EXPECTS(
|
||||
Number(vault->at(sfAssetsTotal)) == c.assetsTotal,
|
||||
std::string(c.name) +
|
||||
@@ -127,8 +107,7 @@ private:
|
||||
for (auto const mode : modes)
|
||||
{
|
||||
NumberRoundModeGuard const rg(mode);
|
||||
auto const result = viaClampVaultOutflow ? clampVaultOutflow(vault, delta)
|
||||
: clampToAssetsTotalScale(vault, delta);
|
||||
auto const result = clampToAssetsTotalScale(vault, delta);
|
||||
|
||||
// The function must be insensitive to the caller's ambient
|
||||
// rounding mode: every mode must agree with the first one
|
||||
@@ -172,13 +151,7 @@ private:
|
||||
continue;
|
||||
}
|
||||
|
||||
// c.expected is guaranteed set here: the !c.expected case
|
||||
// above always continues to the next loop iteration.
|
||||
STAmount const expected{
|
||||
asset,
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
*c.expected,
|
||||
};
|
||||
STAmount const expected{asset, *c.expected};
|
||||
if (!BEAST_EXPECTS(
|
||||
result.has_value(),
|
||||
std::string(c.name) + ": expected success (" + expected.getText() +
|
||||
@@ -203,9 +176,9 @@ private:
|
||||
// representable at that scale.
|
||||
//
|
||||
// For debits this holds directly at postScale, because the
|
||||
// result IS roundToScale(magnitude, postScale, Downward) by
|
||||
// result IS `roundToScale(magnitude, postScale, Downward)` by
|
||||
// construction. For credits the result is
|
||||
// roundedPosterior - assetsTotal, where roundedPosterior
|
||||
// `roundedPosterior - assetsTotal`, where roundedPosterior
|
||||
// sits exactly on the postScale grid but assetsTotal sits on
|
||||
// its own (possibly finer) natural grid; the difference of a
|
||||
// multiple of 10^postScale and a multiple of 10^assetsScale
|
||||
@@ -374,7 +347,7 @@ private:
|
||||
// = 4.999999999999991.
|
||||
// This mirrors testBugVaultDepositOvercreditsAcrossScaleBoundary
|
||||
// in VaultBugs_test.cpp (same seed/deposit values), which
|
||||
// asserts post-fix credited <= paid rather than an exact
|
||||
// asserts post-fix `credited <= paid` rather than an exact
|
||||
// number; this row pins the exact value.
|
||||
.name = "IOU credit: overcredit fix across a scale boundary",
|
||||
.assetsTotal = Number{9'999'999'999'999'999LL, -15},
|
||||
@@ -490,100 +463,6 @@ private:
|
||||
runCases(xrp, xrpCases);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// FixedPrecision vaults, credits: clampToAssetsTotalScale takes the
|
||||
// roundToPosteriorVaultScale branch instead of the Legacy/CashBasis
|
||||
// scale()-of-the-sum branch; it is FixedPrecision's only credit path
|
||||
// (VaultDeposit). All rows below use a Scale-6 vault (baseScale -6)
|
||||
// with assetsTotal already sitting on that grid, so TowardsZero-
|
||||
// truncating delta itself to scale -6 is the whole story: no case
|
||||
// here forces liveScale to coarsen past baseScale (that scenario needs
|
||||
// a non-unit share price and is exercised by
|
||||
// VaultFixedPrecision_test.cpp's testPartialTowardZeroRounding / dust
|
||||
// tests through real transactions instead).
|
||||
// -------------------------------------------------------------------
|
||||
void
|
||||
testFixedPrecisionCredit(Asset const& iou)
|
||||
{
|
||||
std::uint8_t const fixedScale = 6;
|
||||
Number const onGrid{3'234'567, -6}; // 3.234567, exact at scale -6.
|
||||
|
||||
std::array<Case, 3> const cases{
|
||||
Case{
|
||||
// delta already exact at the base grid: passes through
|
||||
// unchanged, same as a Legacy on-grid credit.
|
||||
.name = "FixedPrecision credit: exact on the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{2, -6},
|
||||
.expected = Number{2, -6},
|
||||
},
|
||||
Case{
|
||||
// delta = +2.3 base units, truncates to 2 base units.
|
||||
.name = "FixedPrecision credit: truncated toward zero on the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{23, -7},
|
||||
.expected = Number{2, -6},
|
||||
},
|
||||
Case{
|
||||
// delta = +0.4 base units: sub-ULP at the fixed grid, so
|
||||
// TowardsZero truncates it entirely to zero.
|
||||
.name = "FixedPrecision credit: sub-ULP dust rejected at the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{4, -7},
|
||||
.expected = std::nullopt,
|
||||
},
|
||||
};
|
||||
|
||||
runCases(iou, cases, fixedScale);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// FixedPrecision vaults, debits: outflows (VaultWithdraw, VaultClawback)
|
||||
// never reach clampToAssetsTotalScale's FixedPrecision branch, which
|
||||
// only accepts non-negative deltas; they go through the public
|
||||
// clampVaultOutflow dispatcher instead, which rounds at
|
||||
// AssetsAvailable's own posterior scale (clampToAvailableScale). These
|
||||
// rows pin the same truncation behavior through that entry point.
|
||||
// -------------------------------------------------------------------
|
||||
void
|
||||
testFixedPrecisionDebit(Asset const& iou)
|
||||
{
|
||||
std::uint8_t const fixedScale = 6;
|
||||
Number const onGrid{3'234'567, -6}; // 3.234567, exact at scale -6.
|
||||
|
||||
std::array<Case, 3> const cases{
|
||||
Case{
|
||||
// delta already exact at the base grid: passes through
|
||||
// unchanged, same as a Legacy on-grid debit.
|
||||
.name = "FixedPrecision debit: exact on the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{-1, -6},
|
||||
.expected = Number{1, -6},
|
||||
},
|
||||
Case{
|
||||
// delta = -1.7 base units. TowardsZero truncates the
|
||||
// magnitude to 1 base unit -- this is the "books the
|
||||
// truncated amount" behavior VaultFixedPrecision_test's
|
||||
// testPartialTowardZeroRounding exercises end-to-end via
|
||||
// VaultWithdraw; this row pins it at the helper level.
|
||||
.name = "FixedPrecision debit: truncated toward zero on the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{-17, -7},
|
||||
.expected = Number{1, -6},
|
||||
},
|
||||
Case{
|
||||
// delta = -0.3 base units: sub-ULP at the fixed grid, so
|
||||
// TowardsZero truncates it entirely to zero.
|
||||
.name = "FixedPrecision debit: sub-ULP dust rejected at the base grid",
|
||||
.assetsTotal = onGrid,
|
||||
.delta = Number{-3, -7},
|
||||
.expected = std::nullopt,
|
||||
},
|
||||
};
|
||||
|
||||
runCases(iou, cases, fixedScale, /* viaClampVaultOutflow */ true);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -598,8 +477,6 @@ public:
|
||||
testIouDebits(iou);
|
||||
testIouCredits(iou);
|
||||
testIntegralAssets(mpt, xrp);
|
||||
testFixedPrecisionCredit(iou);
|
||||
testFixedPrecisionDebit(iou);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1052,15 +1052,13 @@ private:
|
||||
}
|
||||
|
||||
void
|
||||
testAssetsMaximum(FeatureBitset features)
|
||||
testAssetsMaximum()
|
||||
{
|
||||
bool const withFix = features[fixCleanup3_5_0];
|
||||
testcase(
|
||||
std::string("Assets Maximum (") + (withFix ? "post" : "pre") + "-fixCleanup3_5_0)");
|
||||
testcase("Assets Maximum");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
|
||||
@@ -1086,10 +1084,6 @@ private:
|
||||
auto const initialXRPPlus1 = to_string(kInitialXrp + 1);
|
||||
BEAST_EXPECT(initialXRPPlus1 == "100000000000000001");
|
||||
|
||||
// Without the fix, rounding an out-of-range AssetsMaximum to the vault
|
||||
// asset throws.
|
||||
TER const outOfRangeErr = withFix ? TER{temMALFORMED} : TER{tefEXCEPTION};
|
||||
|
||||
{
|
||||
testcase("Assets Maximum: XRP");
|
||||
|
||||
@@ -1099,11 +1093,11 @@ private:
|
||||
tx[sfData] = "4D65746144617461";
|
||||
|
||||
tx[sfAssetsMaximum] = maxInt64;
|
||||
env(tx, Ter(outOfRangeErr));
|
||||
env(tx, Ter(tefEXCEPTION));
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = initialXRPPlus1;
|
||||
env(tx, Ter(outOfRangeErr));
|
||||
env(tx, Ter(tefEXCEPTION));
|
||||
env.close();
|
||||
|
||||
tx[sfAssetsMaximum] = initialXRP;
|
||||
@@ -1352,7 +1346,7 @@ private:
|
||||
// What _can't_ IOUs do?
|
||||
// 1. Exceed maximum exponent / offset
|
||||
tx[sfAssetsMaximum] = "1000000000000000e81";
|
||||
env(tx, Ter(outOfRangeErr));
|
||||
env(tx, Ter(tefEXCEPTION));
|
||||
env.close();
|
||||
|
||||
// 2. Mantissa larger than uint64 max
|
||||
@@ -1377,8 +1371,7 @@ public:
|
||||
run() override
|
||||
{
|
||||
testScaleIOU();
|
||||
testAssetsMaximum(all_ - fixCleanup3_5_0);
|
||||
testAssetsMaximum(all_);
|
||||
testAssetsMaximum();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -30,13 +30,11 @@
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -1181,62 +1179,6 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testAssetsMaximumOutOfRange(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
bool const withFix = features[fixCleanup3_5_0];
|
||||
testcase(
|
||||
std::string("AssetsMaximum out of range for the asset (") + (withFix ? "post" : "pre") +
|
||||
"-fixCleanup3_5_0)");
|
||||
|
||||
Env env{*this, features};
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
env.fund(XRP(1'000'000), issuer, owner);
|
||||
env.close();
|
||||
|
||||
Vault const vault{env};
|
||||
|
||||
// Without the fix, rounding an out-of-range AssetsMaximum to the vault
|
||||
// asset throws.
|
||||
TER const createErr = withFix ? TER{temMALFORMED} : TER{tefEXCEPTION};
|
||||
TER const setErr = withFix ? TER{tecPRECISION_LOSS} : TER{tefEXCEPTION};
|
||||
|
||||
auto const test = [&](Asset const& asset, Number const& maxValid, Number const& overMax) {
|
||||
{
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
tx[sfAssetsMaximum] = to_string(overMax);
|
||||
env(tx, Ter(createErr));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet));
|
||||
}
|
||||
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
tx[sfAssetsMaximum] = to_string(maxValid);
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
auto setTx = vault.set({.owner = owner, .id = keylet.key});
|
||||
setTx[sfAssetsMaximum] = to_string(overMax);
|
||||
env(setTx, Ter(setErr));
|
||||
env.close();
|
||||
|
||||
if (auto const sle = env.le(keylet); BEAST_EXPECT(sle))
|
||||
BEAST_EXPECT(sle->at(sfAssetsMaximum) == maxValid);
|
||||
};
|
||||
|
||||
Number const maxXrp{static_cast<std::int64_t>(STAmount::kMaxNativeN)};
|
||||
test(xrpIssue(), maxXrp, maxXrp + 1);
|
||||
|
||||
MPT const btc = MPTTester({.env = env, .issuer = issuer, .holders = {owner}});
|
||||
test(btc.asset(), Number{static_cast<std::int64_t>(kMaxMpTokenAmount)}, Number{1, 19});
|
||||
|
||||
Number const maxIou{static_cast<std::int64_t>(STAmount::kMaxValue), STAmount::kMaxOffset};
|
||||
test(issuer["USD"], maxIou, Number{1, STAmount::kMaxOffset + 16});
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -1250,9 +1192,6 @@ public:
|
||||
|
||||
testVaultWithdrawPseudoAccountDestination(all_ - fixCleanup3_4_0);
|
||||
testVaultWithdrawPseudoAccountDestination(all_);
|
||||
|
||||
testAssetsMaximumOutOfRange(all_ - fixCleanup3_5_0);
|
||||
testAssetsMaximumOutOfRange(all_);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/PathAsset.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -363,6 +365,35 @@ getAccountLines(Env& env, AccountID const& acctId, IOU... ious)
|
||||
[[nodiscard]] bool
|
||||
checkArraySize(json::Value const& val, unsigned int size);
|
||||
|
||||
/**
|
||||
* Reports whether @p reply carries the version 2 envelope: `jsonrpc` equal to
|
||||
* rpc::kJsonRpcVersion and `ripplerpc` equal to rpc::kRippleRpcVersion2, both
|
||||
* present.
|
||||
*
|
||||
* @param reply The reply to inspect.
|
||||
* @return True when both members are present with those values.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
inline bool
|
||||
hasEnvelope2(json::Value const& reply)
|
||||
{
|
||||
return reply.isMember(jss::jsonrpc) && reply[jss::jsonrpc] == rpc::kJsonRpcVersion &&
|
||||
reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc] == rpc::kRippleRpcVersion2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the version 2 envelope on @p request: `jsonrpc` to rpc::kJsonRpcVersion
|
||||
* and `ripplerpc` to rpc::kRippleRpcVersion2.
|
||||
*
|
||||
* @param request The request to stamp.
|
||||
*/
|
||||
inline void
|
||||
setEnvelope2(json::Value& request)
|
||||
{
|
||||
request[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
request[jss::ripplerpc] = rpc::kRippleRpcVersion2;
|
||||
}
|
||||
|
||||
// Helper function that returns the owner count on an account.
|
||||
std::uint32_t
|
||||
ownerCount(test::jtx::Env const& env, test::jtx::Account const& account);
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
|
||||
@@ -163,8 +165,8 @@ public:
|
||||
jr[jss::method] = cmd;
|
||||
if (rpcVersion_ == 2)
|
||||
{
|
||||
jr[jss::jsonrpc] = "2.0";
|
||||
jr[jss::ripplerpc] = "2.0";
|
||||
jr[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
jr[jss::ripplerpc] = rpc::kRippleRpcVersion2;
|
||||
jr[jss::id] = 5;
|
||||
}
|
||||
if (params)
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
|
||||
@@ -207,8 +209,8 @@ public:
|
||||
if (rpcVersion_ == 2)
|
||||
{
|
||||
jp[jss::method] = cmd;
|
||||
jp[jss::jsonrpc] = "2.0";
|
||||
jp[jss::ripplerpc] = "2.0";
|
||||
jp[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
jp[jss::ripplerpc] = rpc::kRippleRpcVersion2;
|
||||
jp[jss::id] = 5;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/multisign.h>
|
||||
@@ -371,8 +372,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
info.isMember(jss::result) && info[jss::result].isMember(jss::account_data));
|
||||
BEAST_EXPECT(!info[jss::result][jss::account_data].isMember(jss::signer_lists));
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -385,8 +385,7 @@ public:
|
||||
auto const& signerLists = data[jss::signer_lists];
|
||||
BEAST_EXPECT(signerLists.isArray());
|
||||
BEAST_EXPECT(signerLists.size() == 0);
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
|
||||
}
|
||||
{
|
||||
@@ -396,8 +395,7 @@ public:
|
||||
info[0u].isMember(jss::result) &&
|
||||
info[0u][jss::result].isMember(jss::account_data));
|
||||
BEAST_EXPECT(!info[0u][jss::result][jss::account_data].isMember(jss::signer_lists));
|
||||
BEAST_EXPECT(info[0u].isMember(jss::jsonrpc) && info[0u][jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info[0u].isMember(jss::ripplerpc) && info[0u][jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info[0u]));
|
||||
BEAST_EXPECT(info[0u].isMember(jss::id) && info[0u][jss::id] == 5);
|
||||
|
||||
BEAST_EXPECT(
|
||||
@@ -408,8 +406,7 @@ public:
|
||||
auto const& signerLists = data[jss::signer_lists];
|
||||
BEAST_EXPECT(signerLists.isArray());
|
||||
BEAST_EXPECT(signerLists.size() == 0);
|
||||
BEAST_EXPECT(info[1u].isMember(jss::jsonrpc) && info[1u][jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info[1u].isMember(jss::ripplerpc) && info[1u][jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info[1u]));
|
||||
BEAST_EXPECT(info[1u].isMember(jss::id) && info[1u][jss::id] == 6);
|
||||
}
|
||||
|
||||
@@ -424,8 +421,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
info.isMember(jss::result) && info[jss::result].isMember(jss::account_data));
|
||||
BEAST_EXPECT(!info[jss::result][jss::account_data].isMember(jss::signer_lists));
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -445,8 +441,7 @@ public:
|
||||
BEAST_EXPECT(signerEntries.size() == 1);
|
||||
auto const& entry0 = signerEntries[0u][sfSignerEntry.jsonName];
|
||||
BEAST_EXPECT(entry0[sfSignerWeight.jsonName] == 3);
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
|
||||
}
|
||||
|
||||
@@ -495,8 +490,7 @@ public:
|
||||
BEAST_EXPECT(entry.isMember(sfAccount.jsonName));
|
||||
BEAST_EXPECT(entry[sfSignerWeight.jsonName] == 1);
|
||||
}
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,25 +797,21 @@ public:
|
||||
// account_lines with mal-formed json2 (missing id field).
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
}
|
||||
{
|
||||
// account_lines with no account.
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::missingFieldError(jss::account)[jss::error_message]);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -824,16 +820,14 @@ public:
|
||||
params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcActMalformed)[jss::error_message]);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
Account const alice{"alice"};
|
||||
@@ -843,16 +837,14 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcActNotFound)[jss::error_message]);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
env.fund(XRP(10000), alice);
|
||||
@@ -869,16 +861,14 @@ public:
|
||||
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'.");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
};
|
||||
|
||||
@@ -895,15 +885,13 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -913,16 +901,14 @@ public:
|
||||
params[jss::ledger_index] = "nonsense";
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
"Invalid field 'ledger_index', not string or number.");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -932,14 +918,12 @@ public:
|
||||
params[jss::ledger_index] = 50000;
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
// Create trust lines to share with alice.
|
||||
@@ -992,44 +976,38 @@ public:
|
||||
BEAST_EXPECT(ledger58Info.seq == 58);
|
||||
|
||||
// A re-usable test for historic ledgers.
|
||||
auto const testAccountLinesHistory = [this, &env](
|
||||
Account const& account,
|
||||
LedgerHeader const& info,
|
||||
int count) {
|
||||
// Get account_lines by ledger index.
|
||||
json::Value paramsSeq;
|
||||
paramsSeq[jss::account] = account.human();
|
||||
paramsSeq[jss::ledger_index] = info.seq;
|
||||
json::Value requestSeq;
|
||||
requestSeq[jss::method] = "account_lines";
|
||||
requestSeq[jss::jsonrpc] = "2.0";
|
||||
requestSeq[jss::ripplerpc] = "2.0";
|
||||
requestSeq[jss::id] = 5;
|
||||
requestSeq[jss::params] = paramsSeq;
|
||||
auto const linesSeq = env.rpc("json2", to_string(requestSeq));
|
||||
BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
|
||||
BEAST_EXPECT(linesSeq.isMember(jss::jsonrpc) && linesSeq[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesSeq.isMember(jss::ripplerpc) && linesSeq[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
|
||||
auto const testAccountLinesHistory =
|
||||
[this, &env](Account const& account, LedgerHeader const& info, int count) {
|
||||
// Get account_lines by ledger index.
|
||||
json::Value paramsSeq;
|
||||
paramsSeq[jss::account] = account.human();
|
||||
paramsSeq[jss::ledger_index] = info.seq;
|
||||
json::Value requestSeq;
|
||||
requestSeq[jss::method] = "account_lines";
|
||||
setEnvelope2(requestSeq);
|
||||
requestSeq[jss::id] = 5;
|
||||
requestSeq[jss::params] = paramsSeq;
|
||||
auto const linesSeq = env.rpc("json2", to_string(requestSeq));
|
||||
BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
|
||||
BEAST_EXPECT(hasEnvelope2(linesSeq));
|
||||
BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
|
||||
|
||||
// Get account_lines by ledger hash.
|
||||
json::Value paramsHash;
|
||||
paramsHash[jss::account] = account.human();
|
||||
paramsHash[jss::ledger_hash] = to_string(info.hash);
|
||||
json::Value requestHash;
|
||||
requestHash[jss::method] = "account_lines";
|
||||
requestHash[jss::jsonrpc] = "2.0";
|
||||
requestHash[jss::ripplerpc] = "2.0";
|
||||
requestHash[jss::id] = 5;
|
||||
requestHash[jss::params] = paramsHash;
|
||||
auto const linesHash = env.rpc("json2", to_string(requestHash));
|
||||
BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
|
||||
BEAST_EXPECT(linesHash.isMember(jss::jsonrpc) && linesHash[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesHash.isMember(jss::ripplerpc) && linesHash[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
|
||||
};
|
||||
// Get account_lines by ledger hash.
|
||||
json::Value paramsHash;
|
||||
paramsHash[jss::account] = account.human();
|
||||
paramsHash[jss::ledger_hash] = to_string(info.hash);
|
||||
json::Value requestHash;
|
||||
requestHash[jss::method] = "account_lines";
|
||||
setEnvelope2(requestHash);
|
||||
requestHash[jss::id] = 5;
|
||||
requestHash[jss::params] = paramsHash;
|
||||
auto const linesHash = env.rpc("json2", to_string(requestHash));
|
||||
BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
|
||||
BEAST_EXPECT(hasEnvelope2(linesHash));
|
||||
BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
|
||||
};
|
||||
|
||||
// Alice should have no trust lines in ledger 3.
|
||||
testAccountLinesHistory(alice, ledger3Info, 0);
|
||||
@@ -1049,8 +1027,7 @@ public:
|
||||
params[jss::ledger_index] = ledger58Info.seq;
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
@@ -1059,8 +1036,7 @@ public:
|
||||
lines[jss::error][jss::message] ==
|
||||
"Exactly one of 'ledger_hash' or 'ledger_index' can be "
|
||||
"specified.");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1069,15 +1045,13 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1087,15 +1061,13 @@ public:
|
||||
params[jss::peer] = gw1.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1105,16 +1077,14 @@ public:
|
||||
params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcActMalformed)[jss::error_message]);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1124,16 +1094,14 @@ public:
|
||||
params[jss::limit] = -1;
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1143,15 +1111,13 @@ public:
|
||||
paramsA[jss::limit] = 1;
|
||||
json::Value requestA;
|
||||
requestA[jss::method] = "account_lines";
|
||||
requestA[jss::jsonrpc] = "2.0";
|
||||
requestA[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestA);
|
||||
requestA[jss::id] = 5;
|
||||
requestA[jss::params] = paramsA;
|
||||
auto const linesA = env.rpc("json2", to_string(requestA));
|
||||
BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
|
||||
BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesA));
|
||||
BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
|
||||
|
||||
// Pick up from where the marker left off. We should get 51.
|
||||
@@ -1161,15 +1127,13 @@ public:
|
||||
paramsB[jss::marker] = marker;
|
||||
json::Value requestB;
|
||||
requestB[jss::method] = "account_lines";
|
||||
requestB[jss::jsonrpc] = "2.0";
|
||||
requestB[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestB);
|
||||
requestB[jss::id] = 5;
|
||||
requestB[jss::params] = paramsB;
|
||||
auto const linesB = env.rpc("json2", to_string(requestB));
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
|
||||
BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesB));
|
||||
BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
|
||||
|
||||
// Go again from where the marker left off, but set a limit of 3.
|
||||
@@ -1179,15 +1143,13 @@ public:
|
||||
paramsC[jss::marker] = marker;
|
||||
json::Value requestC;
|
||||
requestC[jss::method] = "account_lines";
|
||||
requestC[jss::jsonrpc] = "2.0";
|
||||
requestC[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestC);
|
||||
requestC[jss::id] = 5;
|
||||
requestC[jss::params] = paramsC;
|
||||
auto const linesC = env.rpc("json2", to_string(requestC));
|
||||
BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
|
||||
BEAST_EXPECT(linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesC.isMember(jss::ripplerpc) && linesC[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesC));
|
||||
BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5);
|
||||
|
||||
// Mess with the marker so it becomes bad and check for the error.
|
||||
@@ -1197,16 +1159,14 @@ public:
|
||||
paramsD[jss::marker] = marker;
|
||||
json::Value requestD;
|
||||
requestD[jss::method] = "account_lines";
|
||||
requestD[jss::jsonrpc] = "2.0";
|
||||
requestD[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestD);
|
||||
requestD[jss::id] = 5;
|
||||
requestD[jss::params] = paramsD;
|
||||
auto const linesD = env.rpc("json2", to_string(requestD));
|
||||
BEAST_EXPECT(
|
||||
linesD[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
BEAST_EXPECT(linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesD.isMember(jss::ripplerpc) && linesD[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesD));
|
||||
BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1216,16 +1176,14 @@ public:
|
||||
params[jss::marker] = true;
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::expectedFieldMessage(jss::marker, "string"));
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1236,8 +1194,7 @@ public:
|
||||
params[jss::peer] = gw2.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
@@ -1246,8 +1203,7 @@ public:
|
||||
BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
|
||||
BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
|
||||
BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1258,8 +1214,7 @@ public:
|
||||
paramsA[jss::peer] = alice.human();
|
||||
json::Value requestA;
|
||||
requestA[jss::method] = "account_lines";
|
||||
requestA[jss::jsonrpc] = "2.0";
|
||||
requestA[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestA);
|
||||
requestA[jss::id] = 5;
|
||||
requestA[jss::params] = paramsA;
|
||||
auto const linesA = env.rpc("json2", to_string(requestA));
|
||||
@@ -1268,8 +1223,7 @@ public:
|
||||
BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
|
||||
BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
|
||||
BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
|
||||
BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesA));
|
||||
BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
|
||||
|
||||
// Continue from the returned marker to make sure that works.
|
||||
@@ -1282,16 +1236,14 @@ public:
|
||||
paramsB[jss::peer] = alice.human();
|
||||
json::Value requestB;
|
||||
requestB[jss::method] = "account_lines";
|
||||
requestB[jss::jsonrpc] = "2.0";
|
||||
requestB[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestB);
|
||||
requestB[jss::id] = 5;
|
||||
requestB[jss::params] = paramsB;
|
||||
auto const linesB = env.rpc("json2", to_string(requestB));
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
|
||||
BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
|
||||
BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesB));
|
||||
BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -1351,15 +1303,13 @@ public:
|
||||
linesBegParams[jss::limit] = 2;
|
||||
json::Value linesBegRequest;
|
||||
linesBegRequest[jss::method] = "account_lines";
|
||||
linesBegRequest[jss::jsonrpc] = "2.0";
|
||||
linesBegRequest[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(linesBegRequest);
|
||||
linesBegRequest[jss::id] = 5;
|
||||
linesBegRequest[jss::params] = linesBegParams;
|
||||
auto const linesBeg = env.rpc("json2", to_string(linesBegRequest));
|
||||
BEAST_EXPECT(linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
|
||||
BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
|
||||
BEAST_EXPECT(linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesBeg.isMember(jss::ripplerpc) && linesBeg[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesBeg));
|
||||
BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5);
|
||||
|
||||
// alice pays 100 USD to cheri.
|
||||
@@ -1373,16 +1323,14 @@ public:
|
||||
linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker];
|
||||
json::Value linesEndRequest;
|
||||
linesEndRequest[jss::method] = "account_lines";
|
||||
linesEndRequest[jss::jsonrpc] = "2.0";
|
||||
linesEndRequest[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(linesEndRequest);
|
||||
linesEndRequest[jss::id] = 5;
|
||||
linesEndRequest[jss::params] = linesEndParams;
|
||||
auto const linesEnd = env.rpc("json2", to_string(linesEndRequest));
|
||||
BEAST_EXPECT(
|
||||
linesEnd[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
BEAST_EXPECT(linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesEnd.isMember(jss::ripplerpc) && linesEnd[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesEnd));
|
||||
BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5);
|
||||
}
|
||||
|
||||
|
||||
+21
-40
@@ -1,4 +1,5 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/domain.h>
|
||||
@@ -88,8 +89,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -126,8 +126,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -165,8 +164,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -209,8 +207,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -242,8 +239,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -288,8 +284,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -328,8 +323,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -386,8 +380,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -428,8 +421,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -487,8 +479,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -542,8 +533,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -613,8 +603,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -657,8 +646,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -731,8 +719,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -789,8 +776,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -887,8 +873,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -921,8 +906,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -952,8 +936,7 @@ public:
|
||||
auto jv = wsc->invoke("book_offers", jvParams);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
auto jrr = jv[jss::result];
|
||||
@@ -997,8 +980,7 @@ public:
|
||||
jv = wsc->invoke("book_offers", jvParams);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
jrr = jv[jss::result];
|
||||
@@ -1023,8 +1005,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h>
|
||||
@@ -22,6 +23,7 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::test {
|
||||
@@ -254,8 +256,7 @@ class DeliveredAmount_test : public beast::unit_test::Suite
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 3);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
@@ -85,8 +86,7 @@ public:
|
||||
expect(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
expect(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
expect(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
expect(hasEnvelope2(jv));
|
||||
expect(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class MaskSecrets_test : public beast::unit_test::Suite
|
||||
{
|
||||
static constexpr char const* kMasked = "<masked>";
|
||||
static constexpr char const* kSensitive = "sensitive";
|
||||
|
||||
public:
|
||||
/**
|
||||
* Every field in `kCredentialFields` is masked, at the top level.
|
||||
*/
|
||||
void
|
||||
testEveryCredentialField()
|
||||
{
|
||||
testcase("Every credential field is masked");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
request[std::string{field}] = kSensitive;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
BEAST_EXPECTS(masked[std::string{field}] == kMasked, std::string{field});
|
||||
|
||||
// An empty list would let every assertion above pass while masking nothing.
|
||||
BEAST_EXPECT(!rpc::kCredentialFields.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* A field the list does not name is left as it was.
|
||||
*/
|
||||
void
|
||||
testNonCredentialsSurvive()
|
||||
{
|
||||
testcase("Fields that are not credentials are left alone");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::method] = "sign";
|
||||
request[jss::account] = "rSomeAccount";
|
||||
request[jss::secret] = kSensitive;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
BEAST_EXPECT(masked[jss::secret] == kMasked);
|
||||
BEAST_EXPECT(masked[jss::method] == "sign");
|
||||
BEAST_EXPECT(masked[jss::account] == "rSomeAccount");
|
||||
|
||||
// Masking replaces members rather than adding or removing them.
|
||||
BEAST_EXPECT(masked.size() == request.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* A member name is matched whole, not as a substring or case-insensitively.
|
||||
*/
|
||||
void
|
||||
testNamesAreMatchedExactly()
|
||||
{
|
||||
testcase("Only an exact field name is masked");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
for (auto const* name : {"secrets", "Secret", "my_secret", "seedling", "sec", ""})
|
||||
request[name] = kSensitive;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
for (auto const* name : {"secrets", "Secret", "my_secret", "seedling", "sec", ""})
|
||||
BEAST_EXPECTS(masked[name] == kSensitive, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* A credential is masked wherever it sits, not only at the top level.
|
||||
*
|
||||
* The JSON-RPC transport nests a request's fields inside `params`, which is
|
||||
* where a `secret` arrives.
|
||||
*/
|
||||
void
|
||||
testNesting()
|
||||
{
|
||||
testcase("A nested credential is masked at any depth");
|
||||
|
||||
// Where the JSON-RPC transport carries a credential: inside `params`.
|
||||
{
|
||||
json::Value params(json::ValueType::Object);
|
||||
params[jss::secret] = kSensitive;
|
||||
params[jss::account] = "rSomeAccount";
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::method] = "sign";
|
||||
request[jss::params] = json::ValueType::Array;
|
||||
request[jss::params][0u] = params;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
BEAST_EXPECT(masked[jss::params][0u][jss::secret] == kMasked);
|
||||
BEAST_EXPECT(masked[jss::params][0u][jss::account] == "rSomeAccount");
|
||||
BEAST_EXPECT(masked[jss::method] == "sign");
|
||||
}
|
||||
|
||||
// Where a batch carries one: inside an entry, itself inside `params`.
|
||||
{
|
||||
json::Value entry(json::ValueType::Object);
|
||||
entry[jss::id] = 2;
|
||||
entry[jss::seed] = kSensitive;
|
||||
|
||||
json::Value batch(json::ValueType::Object);
|
||||
batch[jss::method] = "batch";
|
||||
batch[jss::params] = json::ValueType::Array;
|
||||
batch[jss::params][0u] = entry;
|
||||
|
||||
auto const masked = rpc::maskSecrets(batch);
|
||||
BEAST_EXPECT(masked[jss::params][0u][jss::seed] == kMasked);
|
||||
BEAST_EXPECT(masked[jss::params][0u][jss::id] == 2);
|
||||
}
|
||||
|
||||
// A credential in an array element, which names no member of its own.
|
||||
{
|
||||
json::Value inner(json::ValueType::Object);
|
||||
inner[jss::passphrase] = kSensitive;
|
||||
|
||||
json::Value request(json::ValueType::Array);
|
||||
request[0u] = inner;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
BEAST_EXPECT(masked[0u][jss::passphrase] == kMasked);
|
||||
}
|
||||
|
||||
// Up to the parser's nesting limit, which is what bounds the recursion.
|
||||
{
|
||||
json::Value deep(json::ValueType::Object);
|
||||
deep[jss::secret] = kSensitive;
|
||||
for (unsigned i{0}; i < json::Reader::kNestLimit; ++i)
|
||||
{
|
||||
json::Value outer(json::ValueType::Object);
|
||||
outer[jss::params] = deep;
|
||||
deep = outer;
|
||||
}
|
||||
|
||||
auto const masked = rpc::maskSecrets(deep);
|
||||
auto const* level = &masked;
|
||||
for (unsigned i{0}; i < json::Reader::kNestLimit; ++i)
|
||||
level = &(*level)[jss::params];
|
||||
BEAST_EXPECT((*level)[jss::secret] == kMasked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A credential's own value is replaced whole, whatever shape it has.
|
||||
*
|
||||
* An object or array under `secret` is not walked into: the whole value is
|
||||
* the credential.
|
||||
*/
|
||||
void
|
||||
testCredentialIsNotDescendedInto()
|
||||
{
|
||||
testcase("A credential is replaced whatever it holds");
|
||||
|
||||
json::Value nested(json::ValueType::Object);
|
||||
nested["inner"] = kSensitive;
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::secret] = nested;
|
||||
request[jss::seed] = json::ValueType::Array;
|
||||
request[jss::seed][0u] = kSensitive;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
BEAST_EXPECT(masked[jss::secret] == kMasked);
|
||||
BEAST_EXPECT(masked[jss::seed] == kMasked);
|
||||
|
||||
BEAST_EXPECT(!to_string(masked).contains(kSensitive));
|
||||
}
|
||||
|
||||
/**
|
||||
* A credential that is not a string is masked too, the value being replaced
|
||||
* whole.
|
||||
*/
|
||||
void
|
||||
testNonStringCredentials()
|
||||
{
|
||||
testcase("A credential of any type is masked");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request["secret"] = 12345;
|
||||
request["seed"] = true;
|
||||
request["passphrase"] = 1.5;
|
||||
request["password"] = json::ValueType::Null;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
for (auto const* field : {"secret", "seed", "passphrase", "password"})
|
||||
BEAST_EXPECTS(masked[field] == kMasked, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* A value with no members is returned unchanged, masking keying on member
|
||||
* names.
|
||||
*/
|
||||
void
|
||||
testValuesWithoutMembers()
|
||||
{
|
||||
testcase("A value that has no members is returned unchanged");
|
||||
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{}).isNull());
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{kSensitive}) == kSensitive);
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{42}) == 42);
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{json::ValueType::Object}).size() == 0);
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{json::ValueType::Array}).size() == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The request the caller passed in is not modified.
|
||||
*
|
||||
* Every call site goes on to use the request it holds, so the mask copies.
|
||||
*/
|
||||
void
|
||||
testRequestIsNotModified()
|
||||
{
|
||||
testcase("The request passed in is left unchanged");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::secret] = kSensitive;
|
||||
request[jss::params] = json::ValueType::Array;
|
||||
request[jss::params][0u] = json::ValueType::Object;
|
||||
request[jss::params][0u][jss::seed] = kSensitive;
|
||||
|
||||
auto const before = to_string(request);
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
|
||||
BEAST_EXPECT(to_string(request) == before);
|
||||
BEAST_EXPECT(request[jss::secret] == kSensitive);
|
||||
BEAST_EXPECT(request[jss::params][0u][jss::seed] == kSensitive);
|
||||
BEAST_EXPECT(masked[jss::secret] == kMasked);
|
||||
}
|
||||
|
||||
/**
|
||||
* `hasSecret` reports a credential at any depth and nothing else.
|
||||
*
|
||||
* Every name on the list is reported at the top level, one nested where the
|
||||
* JSON-RPC transport puts it and one inside an array element are reported
|
||||
* too, and a value naming none, or having no members at all, is not.
|
||||
*/
|
||||
void
|
||||
testHasSecret()
|
||||
{
|
||||
testcase("A credential is detected at any depth");
|
||||
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
{
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[std::string{field}] = kSensitive;
|
||||
BEAST_EXPECTS(rpc::hasSecret(request), std::string{field});
|
||||
}
|
||||
|
||||
// Inside `params`, where the JSON-RPC transport carries a credential.
|
||||
{
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::method] = "sign";
|
||||
request[jss::params] = json::ValueType::Array;
|
||||
request[jss::params][0u] = json::ValueType::Object;
|
||||
request[jss::params][0u][jss::account] = "rSomeAccount";
|
||||
request[jss::params][0u][jss::secret] = kSensitive;
|
||||
BEAST_EXPECT(rpc::hasSecret(request));
|
||||
}
|
||||
|
||||
// Inside an array element, which names no member of its own.
|
||||
{
|
||||
json::Value request(json::ValueType::Array);
|
||||
request[0u] = json::ValueType::Object;
|
||||
request[0u][jss::passphrase] = kSensitive;
|
||||
BEAST_EXPECT(rpc::hasSecret(request));
|
||||
}
|
||||
|
||||
// No credential at any depth, and a near miss is not one.
|
||||
{
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::method] = "account_info";
|
||||
request[jss::params] = json::ValueType::Array;
|
||||
request[jss::params][0u] = json::ValueType::Object;
|
||||
request[jss::params][0u][jss::account] = "rSomeAccount";
|
||||
request[jss::params][0u]["secrets"] = kSensitive;
|
||||
BEAST_EXPECT(!rpc::hasSecret(request));
|
||||
}
|
||||
|
||||
// A value with no members carries none.
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{}));
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{kSensitive}));
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{42}));
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{json::ValueType::Object}));
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{json::ValueType::Array}));
|
||||
}
|
||||
|
||||
/**
|
||||
* `loggable` masks a value that carries a credential, then truncates at
|
||||
* `kMaxLoggedChars`; one that carries none is rendered as it is.
|
||||
*
|
||||
* The mask has to survive the cap, so the credential is placed where a
|
||||
* sorted member order puts it first.
|
||||
*/
|
||||
void
|
||||
testLoggable()
|
||||
{
|
||||
testcase("A rendering for a log line is masked and capped");
|
||||
|
||||
// Sorts before the filler, so the mask falls inside the cap.
|
||||
json::Value request;
|
||||
request[jss::secret] = kSensitive;
|
||||
request["zfiller"] = std::string(2 * rpc::kMaxLoggedChars, 'z');
|
||||
|
||||
auto const rendered = rpc::loggable(request);
|
||||
|
||||
BEAST_EXPECT(rendered.size() == rpc::kMaxLoggedChars);
|
||||
BEAST_EXPECT(rendered.contains(kMasked));
|
||||
BEAST_EXPECT(!rendered.contains(kSensitive));
|
||||
|
||||
// A short request is not padded.
|
||||
json::Value small;
|
||||
small[jss::seed] = kSensitive;
|
||||
BEAST_EXPECT(rpc::loggable(small) == to_string(rpc::maskSecrets(small)));
|
||||
|
||||
// A value with no credential is the serialized value itself, cut at the cap.
|
||||
json::Value plain;
|
||||
plain[jss::account] = "rSomeAccount";
|
||||
plain["zfiller"] = std::string(2 * rpc::kMaxLoggedChars, 'z');
|
||||
BEAST_EXPECT(rpc::loggable(plain) == to_string(plain).substr(0, rpc::kMaxLoggedChars));
|
||||
BEAST_EXPECT(rpc::loggable(small[jss::seed]) == to_string(small[jss::seed]));
|
||||
}
|
||||
|
||||
/**
|
||||
* A keygen reply is rendered for the log with no key in it.
|
||||
*
|
||||
* Every field of each reply is checked, not only the names on the list, so
|
||||
* a field the list is missing fails here. Only the fields a client may read
|
||||
* in public are allowed through.
|
||||
*/
|
||||
void
|
||||
testKeygenRepliesAreMasked()
|
||||
{
|
||||
testcase("A keygen reply is logged with every key masked");
|
||||
|
||||
static constexpr std::array<std::string_view, 6> kPublicFields{
|
||||
"account_id",
|
||||
"key_type",
|
||||
"public_key",
|
||||
"public_key_hex",
|
||||
"status",
|
||||
"validation_public_key",
|
||||
};
|
||||
|
||||
jtx::Env env{*this};
|
||||
for (auto const command : {"wallet_propose", "validation_create"})
|
||||
{
|
||||
auto const reply = env.rpc(command);
|
||||
auto const& result = reply[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == "success");
|
||||
|
||||
auto const rendered = rpc::loggable(reply);
|
||||
for (auto const& name : result.getMemberNames())
|
||||
{
|
||||
if (std::ranges::find(kPublicFields, name) != kPublicFields.end())
|
||||
continue;
|
||||
auto const& value = result[name];
|
||||
BEAST_EXPECT(value.isString() && !value.asString().empty());
|
||||
BEAST_EXPECTS(!rendered.contains(value.asString()), command + (": " + name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testEveryCredentialField();
|
||||
testNonCredentialsSurvive();
|
||||
testNamesAreMatchedExactly();
|
||||
testNesting();
|
||||
testCredentialIsNotDescendedInto();
|
||||
testNonStringCredentials();
|
||||
testValuesWithoutMembers();
|
||||
testRequestIsNotModified();
|
||||
testHasSecret();
|
||||
testLoggable();
|
||||
testKeygenRepliesAreMasked();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(MaskSecrets, rpc, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h> // IWYU pragma: keep
|
||||
@@ -38,8 +39,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,7 @@ public:
|
||||
auto jv = wsc->invoke("submit", payment);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefMAX_LEDGER");
|
||||
@@ -65,8 +64,7 @@ public:
|
||||
jv = wsc->invoke("submit", payment);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefPAST_SEQ");
|
||||
@@ -76,8 +74,7 @@ public:
|
||||
jv = wsc->invoke("submit", payment);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "terPRE_SEQ");
|
||||
@@ -87,8 +84,7 @@ public:
|
||||
jv = wsc->invoke("submit", payment);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
@@ -100,8 +96,7 @@ public:
|
||||
jv = wsc->invoke("ledger_accept");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
|
||||
@@ -128,8 +123,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -164,8 +158,7 @@ public:
|
||||
jv = wsc->invoke("submit", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
@@ -187,8 +180,7 @@ public:
|
||||
jv = wsc->invoke("account_tx", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
|
||||
@@ -218,8 +210,7 @@ public:
|
||||
jv = wsc->invoke("submit", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
@@ -228,8 +219,7 @@ public:
|
||||
jv = wsc->invoke("ledger_accept");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
|
||||
@@ -247,8 +237,7 @@ public:
|
||||
jv = wsc->invoke("subscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -260,8 +249,7 @@ public:
|
||||
auto jv = wsc->invoke("ledger_accept");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
|
||||
@@ -281,8 +269,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -300,8 +287,7 @@ public:
|
||||
jv = wsc->invoke("subscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -313,8 +299,7 @@ public:
|
||||
auto jv = wsc->invoke("ledger_accept");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
|
||||
@@ -334,8 +319,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -352,8 +336,7 @@ public:
|
||||
jv = wsc->invoke("account_tx", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
|
||||
@@ -383,8 +366,7 @@ public:
|
||||
jv = wsc->invoke("subscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -399,8 +381,7 @@ public:
|
||||
jv = wsc->invoke("submit", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
@@ -421,8 +402,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/batch.h>
|
||||
@@ -83,8 +84,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -113,8 +113,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -149,8 +148,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 2);
|
||||
@@ -184,8 +182,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -208,8 +205,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -266,8 +262,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -281,8 +276,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -314,8 +308,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -344,8 +337,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -391,8 +383,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -414,8 +405,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -425,8 +415,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -461,8 +450,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -537,8 +525,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/CaptureLogs.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/JSONRPCClient.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/beast/test/yield_to.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/server/LoadFeeTrack.h>
|
||||
#include <xrpl/server/NetworkOPs.h>
|
||||
@@ -37,11 +44,13 @@
|
||||
#include <boost/lexical_cast.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -203,6 +212,43 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
}
|
||||
}
|
||||
|
||||
using Response = boost::beast::http::response<boost::beast::http::string_body>;
|
||||
|
||||
static constexpr auto kOk = boost::beast::http::status::ok;
|
||||
static constexpr auto kBadRequest = boost::beast::http::status::bad_request;
|
||||
|
||||
/**
|
||||
* Posts @p body to the RPC port and returns the reply, parsed.
|
||||
*
|
||||
* @param env The environment naming the port.
|
||||
* @param yield The coroutine the request runs on.
|
||||
* @param resp Receives the whole HTTP response, for a caller asserting on
|
||||
* its status.
|
||||
* @param ec Receives a connection error.
|
||||
* @param body The request to post.
|
||||
* @param label Identifies which case failed, for a caller iterating over
|
||||
* several. An empty one reports what BEAST_EXPECT would.
|
||||
* @param fields Extra headers to send with it.
|
||||
* @return The reply, parsed. Empty when the body is not JSON, which this
|
||||
* asserts against.
|
||||
*/
|
||||
json::Value
|
||||
postAndParse(
|
||||
test::jtx::Env& env,
|
||||
boost::asio::yield_context& yield,
|
||||
Response& resp,
|
||||
boost::system::error_code& ec,
|
||||
std::string const& body,
|
||||
std::string_view label = {},
|
||||
MyFields const& fields = {})
|
||||
{
|
||||
doHTTPRequest(env, yield, false, resp, ec, body, fields);
|
||||
|
||||
json::Value reply;
|
||||
BEAST_EXPECTS(json::Reader{}.parse(resp.body(), reply), label);
|
||||
return reply;
|
||||
}
|
||||
|
||||
void
|
||||
doWSRequest(
|
||||
test::jtx::Env& env,
|
||||
@@ -217,6 +263,60 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
doRequest(yield, makeWSUpgrade(*ip, *port), *ip, *port, secure, resp, ec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends @p body verbatim over a fresh WebSocket session and returns the
|
||||
* first reply, parsed, so a message naming no command can be exercised.
|
||||
*
|
||||
* @param yield The coroutine the request runs on.
|
||||
* @param ip The server address.
|
||||
* @param port The WebSocket port.
|
||||
* @param body The frame to send.
|
||||
* @return The reply, parsed. Empty when the connection fails, which this
|
||||
* asserts against.
|
||||
*/
|
||||
json::Value
|
||||
doWSMessage(
|
||||
boost::asio::yield_context& yield,
|
||||
std::string const& ip,
|
||||
std::uint16_t port,
|
||||
std::string const& body)
|
||||
{
|
||||
using namespace boost::asio;
|
||||
using namespace boost::beast;
|
||||
|
||||
io_context& ios = getIoContext();
|
||||
websocket::stream<ip::tcp::socket> ws{ios};
|
||||
boost::system::error_code ec;
|
||||
|
||||
ip::tcp::resolver r{ios};
|
||||
auto const it = r.async_resolve(ip, std::to_string(port), yield[ec]);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return {};
|
||||
async_connect(ws.next_layer(), it, yield[ec]);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return {};
|
||||
ws.async_handshake(ip + ":" + std::to_string(port), "/", yield[ec]);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return {};
|
||||
|
||||
ws.async_write(buffer(body), yield[ec]);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return {};
|
||||
|
||||
multi_buffer sb;
|
||||
ws.async_read(sb, yield[ec]);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return {};
|
||||
|
||||
std::string text;
|
||||
text.resize(buffer_size(sb.data()));
|
||||
buffer_copy(buffer(text.data(), text.size()), sb.data());
|
||||
|
||||
json::Value reply;
|
||||
BEAST_EXPECT(json::Reader{}.parse(text, reply));
|
||||
return reply;
|
||||
}
|
||||
|
||||
void
|
||||
doHTTPRequest(
|
||||
test::jtx::Env& env,
|
||||
@@ -409,7 +509,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
doHTTPRequest(env, yield, false, resp, ec);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
|
||||
BEAST_EXPECT(resp.result() == kOk);
|
||||
}
|
||||
|
||||
// secure request
|
||||
@@ -419,7 +519,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
doHTTPRequest(env, yield, true, resp, ec);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
|
||||
BEAST_EXPECT(resp.result() == kOk);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,7 +645,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
// finally if we use the correct user/pass encoded, we should get a 200
|
||||
auth.set("Authorization", "Basic " + base64Encode(user + ":" + pass));
|
||||
doHTTPRequest(env, yield, secure, resp, ec, to_string(jr), auth);
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
|
||||
BEAST_EXPECT(resp.result() == kOk);
|
||||
BEAST_EXPECT(!resp.body().empty());
|
||||
}
|
||||
|
||||
@@ -732,6 +832,16 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
auto resp = sendAndParse("NOT JSON");
|
||||
BEAST_EXPECT(resp.isMember(jss::error) && resp[jss::error] == "jsonInvalid");
|
||||
BEAST_EXPECT(!resp.isMember(jss::status));
|
||||
// The body is reported by size, not echoed.
|
||||
BEAST_EXPECT(resp.isMember(jss::size) && resp[jss::size] == 8);
|
||||
BEAST_EXPECT(!resp.isMember(jss::value));
|
||||
}
|
||||
|
||||
{ // an unparsable frame carrying a credential does not echo it
|
||||
auto resp =
|
||||
sendAndParse(R"({"command":"submit","secret":"snoPBrXtMeMyMHUVTgbuqAfg1SUTb",})");
|
||||
BEAST_EXPECT(resp.isMember(jss::error) && resp[jss::error] == "jsonInvalid");
|
||||
BEAST_EXPECT(!to_string(resp).contains("snoPBrXtMeMyMHUVTgbuqAfg1SUTb"));
|
||||
}
|
||||
|
||||
{ // send incorrect json (method and command fields differ)
|
||||
@@ -813,7 +923,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
|
||||
BEAST_EXPECT(resp.result() == kOk);
|
||||
BEAST_EXPECT(resp.body().contains("connectivity is working."));
|
||||
|
||||
// mark the Network as having an Amendment Warning, but won't fail
|
||||
@@ -858,7 +968,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
|
||||
BEAST_EXPECT(resp.result() == kOk);
|
||||
BEAST_EXPECT(resp.body().contains("connectivity is working."));
|
||||
|
||||
// with ELB_SUPPORT, status still does not indicate a problem
|
||||
@@ -878,7 +988,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
|
||||
BEAST_EXPECT(resp.result() == kOk);
|
||||
BEAST_EXPECT(resp.body().contains("connectivity is working."));
|
||||
}
|
||||
|
||||
@@ -941,7 +1051,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
|
||||
BEAST_EXPECT(resp.result() == kOk);
|
||||
BEAST_EXPECT(resp.body().contains("connectivity is working."));
|
||||
|
||||
// mark the Network as Amendment Blocked, but still won't fail until
|
||||
@@ -989,7 +1099,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::ok);
|
||||
BEAST_EXPECT(resp.result() == kOk);
|
||||
BEAST_EXPECT(resp.body().contains("connectivity is working."));
|
||||
|
||||
env.app().config().elbSupport = true;
|
||||
@@ -1025,7 +1135,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
{
|
||||
boost::beast::http::response<boost::beast::http::string_body> resp;
|
||||
doHTTPRequest(env, yield, false, resp, ec, "{}");
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n");
|
||||
}
|
||||
|
||||
@@ -1034,7 +1144,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
json::Value jv;
|
||||
jv["invalid"] = 1;
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "Null method\r\n");
|
||||
}
|
||||
|
||||
@@ -1043,7 +1153,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
json::Value jv(json::ValueType::Array);
|
||||
jv.append("invalid");
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n");
|
||||
}
|
||||
|
||||
@@ -1054,7 +1164,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
j["invalid"] = 1;
|
||||
jv.append(j);
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "Unable to parse request: \r\n");
|
||||
}
|
||||
|
||||
@@ -1064,7 +1174,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
jv[jss::method] = "batch";
|
||||
jv[jss::params] = 2;
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "Malformed batch request\r\n");
|
||||
}
|
||||
|
||||
@@ -1075,7 +1185,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
jv[jss::params] = json::ValueType::Object;
|
||||
jv[jss::params]["invalid"] = 3;
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "Malformed batch request\r\n");
|
||||
}
|
||||
|
||||
@@ -1084,7 +1194,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
boost::beast::http::response<boost::beast::http::string_body> resp;
|
||||
jv[jss::method] = json::ValueType::Null;
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "Null method\r\n");
|
||||
}
|
||||
|
||||
@@ -1092,7 +1202,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
boost::beast::http::response<boost::beast::http::string_body> resp;
|
||||
jv[jss::method] = 1;
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "method is not string\r\n");
|
||||
}
|
||||
|
||||
@@ -1100,7 +1210,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
boost::beast::http::response<boost::beast::http::string_body> resp;
|
||||
jv[jss::method] = "";
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "method is empty\r\n");
|
||||
}
|
||||
|
||||
@@ -1109,7 +1219,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
jv[jss::method] = "some_method";
|
||||
jv[jss::params] = "params";
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "params unparsable\r\n");
|
||||
}
|
||||
|
||||
@@ -1118,11 +1228,564 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
jv[jss::params][0u] = "not an object";
|
||||
doHTTPRequest(env, yield, false, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(resp.result() == boost::beast::http::status::bad_request);
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
BEAST_EXPECT(resp.body() == "params unparsable\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `ripplerpc: "3.0"` envelope reports 200 for the codes below.
|
||||
*
|
||||
* `account_info` on an account the ledger does not hold is a routine call,
|
||||
* and a 4xx there turns a working reply into a failure for anything that
|
||||
* fails over on one. The status each code names in the table is pinned in
|
||||
* the `ErrorCodes` gtest.
|
||||
*
|
||||
* @param yield The coroutine the requests run on.
|
||||
*/
|
||||
void
|
||||
testGainedStatusesStayOffLegacyEnvelope(boost::asio::yield_context& yield)
|
||||
{
|
||||
testcase("A code that gained an HTTP status keeps 200 on the legacy envelope");
|
||||
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->loadFromString(std::string("[") + Sections::kSigningSupport + "]\ntrue");
|
||||
return cfg;
|
||||
})};
|
||||
|
||||
Account const alice{"alice"};
|
||||
// Never funded, so the ledger holds no entry for it.
|
||||
Account const absent{"absent"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
boost::system::error_code ec;
|
||||
|
||||
// Accepted by the signing checks, so the request reaches the two conditions below.
|
||||
// `sign_for` fills nothing in, so the fee and sequence are named here.
|
||||
auto const accountSet = [&env, &alice] {
|
||||
json::Value tx;
|
||||
tx[jss::Account] = alice.human();
|
||||
tx[jss::TransactionType] = jss::AccountSet;
|
||||
tx[jss::SigningPubKey] = "";
|
||||
tx[jss::Fee] = (8 * env.current()->fees().base).jsonClipped();
|
||||
tx[jss::Sequence] = env.seq(alice);
|
||||
return tx;
|
||||
};
|
||||
|
||||
auto const account = [](std::string_view ident) {
|
||||
json::Value params(json::ValueType::Object);
|
||||
params[jss::account] = ident;
|
||||
return params;
|
||||
};
|
||||
|
||||
// `sign` single-signs, so a transaction already carrying `Signers` is multisigned.
|
||||
json::Value multisigned(json::ValueType::Object);
|
||||
multisigned[jss::secret] = toBase58(generateSeed("alice"));
|
||||
multisigned[jss::tx_json] = accountSet();
|
||||
multisigned[jss::tx_json][jss::Signers] = json::ValueType::Array;
|
||||
|
||||
// `sign_for` multisigns, so a transaction already carrying `TxnSignature` is single-signed.
|
||||
json::Value singleSigned(json::ValueType::Object);
|
||||
singleSigned[jss::account] = alice.human();
|
||||
singleSigned[jss::secret] = toBase58(generateSeed("alice"));
|
||||
singleSigned[jss::tx_json] = accountSet();
|
||||
singleSigned[jss::tx_json][jss::TxnSignature] = "DEADBEEF";
|
||||
|
||||
struct Case
|
||||
{
|
||||
char const* method;
|
||||
json::Value params;
|
||||
ErrorCodeI code;
|
||||
};
|
||||
|
||||
for (auto& [method, params, code] : {
|
||||
Case{
|
||||
.method = "account_info",
|
||||
.params = account("bogus"),
|
||||
.code = RpcActMalformed,
|
||||
},
|
||||
Case{
|
||||
.method = "account_info",
|
||||
.params = account(absent.human()),
|
||||
.code = RpcActNotFound,
|
||||
},
|
||||
Case{.method = "sign", .params = multisigned, .code = RpcAlreadyMultisig},
|
||||
Case{.method = "sign_for", .params = singleSigned, .code = RpcAlreadySingleSig},
|
||||
})
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::method] = method;
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
jv[jss::params][0u] = params;
|
||||
jv[jss::params][0u][jss::ripplerpc] = rpc::kRippleRpcVersion3;
|
||||
|
||||
Response resp;
|
||||
auto const& info = rpc::getErrorInfo(code);
|
||||
auto const label = std::string{info.token.cStr()};
|
||||
|
||||
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv), label);
|
||||
BEAST_EXPECTS(reply[jss::error][jss::error] == info.token.cStr(), label);
|
||||
BEAST_EXPECTS(reply[jss::error][jss::error_code] == code, label);
|
||||
// The code names a status the envelope declines to report, so the two disagree here
|
||||
// by design.
|
||||
BEAST_EXPECTS(info.httpStatus != 200, label);
|
||||
BEAST_EXPECTS(resp.result() == kOk, label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A credential the server echoes back is masked, on every path that echoes.
|
||||
*
|
||||
* Driven from `kCredentialFields` itself, so a field added to the list is
|
||||
* covered here.
|
||||
*
|
||||
* @param yield The coroutine the requests run on.
|
||||
*/
|
||||
void
|
||||
testMaskedCredentials(boost::asio::yield_context& yield)
|
||||
{
|
||||
testcase("Echoed requests have their credentials masked");
|
||||
|
||||
using namespace test::jtx;
|
||||
Env env{*this};
|
||||
|
||||
boost::system::error_code ec;
|
||||
|
||||
// Version 1 echoes the request back on an error, so its credentials must be masked.
|
||||
{
|
||||
boost::beast::http::response<boost::beast::http::string_body> resp;
|
||||
json::Value jv;
|
||||
jv[jss::method] = "sign";
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
json::Value params(json::ValueType::Object);
|
||||
params[jss::ripplerpc] = rpc::kRippleRpcVersion1;
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
params[std::string{field}] = "sensitive";
|
||||
jv[jss::params][0u] = params;
|
||||
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv));
|
||||
auto const& echoed = reply[jss::result][jss::request];
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
BEAST_EXPECTS(echoed[std::string{field}] == "<masked>", std::string{field});
|
||||
}
|
||||
|
||||
// A request the session rejects before dispatch is echoed back, so it must be masked.
|
||||
// Written directly, since the client always names a command.
|
||||
{
|
||||
auto const port = env.app().config()[Sections::kPortWs].get<std::uint16_t>(Keys::kPort);
|
||||
auto const ip = env.app().config()[Sections::kPortWs].get<std::string>(Keys::kIp);
|
||||
|
||||
json::Value jv;
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
jv[std::string{field}] = "sensitive";
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
auto const reply = doWSMessage(yield, *ip, *port, to_string(jv));
|
||||
auto const& echoed = reply[jss::request];
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
BEAST_EXPECTS(echoed[std::string{field}] == "<masked>", std::string{field});
|
||||
}
|
||||
|
||||
// A rejection echoing the whole request must mask the credentials inside
|
||||
// `params`, where the JSON-RPC transport carries them.
|
||||
{
|
||||
json::Value params(json::ValueType::Object);
|
||||
params[jss::account] = "rSomeAccount";
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
params[std::string{field}] = "sensitive";
|
||||
|
||||
json::Value entry;
|
||||
entry[jss::id] = 2;
|
||||
entry[jss::params] = json::ValueType::Array;
|
||||
entry[jss::params][0u] = params;
|
||||
|
||||
// A batch: a lone request naming no method is answered with a bare message. The
|
||||
// `"method": "batch"` form nests entries under `params` on every version.
|
||||
json::Value batch;
|
||||
batch[jss::method] = "batch";
|
||||
batch[jss::params] = json::ValueType::Array;
|
||||
batch[jss::params][0u] = entry;
|
||||
|
||||
Response resp;
|
||||
auto const reply = postAndParse(env, yield, resp, ec, to_string(batch));
|
||||
BEAST_EXPECT(reply.isArray() && reply.size() == 1);
|
||||
|
||||
auto const& echoed = reply[0u][jss::params][0u];
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
BEAST_EXPECTS(echoed[std::string{field}] == "<masked>", std::string{field});
|
||||
|
||||
// The rest of the request survives masking; only the credentials are replaced.
|
||||
BEAST_EXPECT(echoed[jss::account] == "rSomeAccount");
|
||||
BEAST_EXPECT(reply[0u][jss::id] == 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The request written to the log is masked, and capped in length.
|
||||
*
|
||||
* Both are asserted on one rendering: without the mask a seed reaches the
|
||||
* log in the clear, and without the cap a client chooses how much it writes
|
||||
* there. The marker past the cap is expected nowhere, every site rendering
|
||||
* through `rpc::loggable`.
|
||||
*
|
||||
* `CaptureLogs` assigns its text in the destructor, so the `Env` is scoped
|
||||
* and the string read after it.
|
||||
*
|
||||
* @param yield The coroutine the request runs on.
|
||||
*/
|
||||
void
|
||||
testTheLoggedRequestIsMaskedAndCapped(boost::asio::yield_context& yield)
|
||||
{
|
||||
testcase("The logged request is masked and capped");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
// Sorts after `secret` and before the tail marker, so the cap falls between them.
|
||||
static constexpr std::size_t kFillerSize = 12000;
|
||||
static constexpr char const* kSensitiveSeed = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb";
|
||||
static constexpr char const* kTailMarker = "PastTheCapMarker";
|
||||
|
||||
std::string logs;
|
||||
{
|
||||
Env env{
|
||||
*this, envconfig(), std::make_unique<CaptureLogs>(&logs), beast::Severity::Debug};
|
||||
|
||||
boost::system::error_code ec;
|
||||
Response resp;
|
||||
|
||||
json::Value params(json::ValueType::Object);
|
||||
params[jss::secret] = kSensitiveSeed;
|
||||
params["zfiller"] = std::string(kFillerSize, 'z');
|
||||
params["zzztail"] = kTailMarker;
|
||||
|
||||
json::Value jv;
|
||||
jv[jss::method] = "ping";
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
jv[jss::params][0u] = params;
|
||||
|
||||
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(reply[jss::result][jss::status] == jss::success);
|
||||
}
|
||||
|
||||
auto const occurrences = [&logs](std::string_view needle) {
|
||||
std::size_t count = 0;
|
||||
for (auto at = logs.find(needle); at != std::string::npos;
|
||||
at = logs.find(needle, at + 1))
|
||||
{
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
// The seed reaches no line at all, and the duration line wrote the mask in its place.
|
||||
BEAST_EXPECT(occurrences(kSensitiveSeed) == 0);
|
||||
BEAST_EXPECT(logs.contains("RPC request processing duration = "));
|
||||
BEAST_EXPECT(occurrences("<masked>") >= 2);
|
||||
|
||||
// No line reached the tail marker, because every site renders through `rpc::loggable`.
|
||||
BEAST_EXPECT(occurrences(kTailMarker) == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The reply written to the log is masked only when it carries a credential.
|
||||
*
|
||||
* A `wallet_propose` reply carries the keys it generated, and its `Reply:`
|
||||
* line holds `<masked>` in their place. A `server_info` reply carries none
|
||||
* and is logged as the string the client received.
|
||||
*
|
||||
* @param yield The coroutine the requests run on.
|
||||
*/
|
||||
void
|
||||
testTheLoggedReplyIsMaskedOnlyWhenItCarriesACredential(boost::asio::yield_context& yield)
|
||||
{
|
||||
testcase("The logged reply is masked only when it carries a credential");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
std::string logs;
|
||||
json::Value wallet;
|
||||
json::Value info;
|
||||
{
|
||||
Env env{
|
||||
*this, envconfig(), std::make_unique<CaptureLogs>(&logs), beast::Severity::Debug};
|
||||
|
||||
boost::system::error_code ec;
|
||||
Response resp;
|
||||
|
||||
json::Value jv;
|
||||
jv[jss::method] = "wallet_propose";
|
||||
wallet = postAndParse(env, yield, resp, ec, to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(wallet[jss::status] == jss::success);
|
||||
|
||||
jv[jss::method] = "server_info";
|
||||
info = postAndParse(env, yield, resp, ec, to_string(jv))[jss::result];
|
||||
BEAST_EXPECT(info[jss::status] == jss::success);
|
||||
}
|
||||
|
||||
BEAST_EXPECT(logs.contains("Reply: "));
|
||||
|
||||
// The keys the client received reach no line, and the mask stands in their place.
|
||||
for (auto const field : {jss::master_seed, jss::master_key})
|
||||
{
|
||||
auto const key = wallet[field].asString();
|
||||
BEAST_EXPECT(!key.empty());
|
||||
BEAST_EXPECTS(!logs.contains(key), field.cStr());
|
||||
}
|
||||
BEAST_EXPECT(logs.contains("<masked>"));
|
||||
|
||||
// A reply with no credential is logged as built: one line carries this member as the
|
||||
// client read it.
|
||||
auto const version = info[jss::info][jss::build_version].asString();
|
||||
BEAST_EXPECT(!version.empty());
|
||||
BEAST_EXPECT(logs.contains("\"build_version\":\"" + version + "\""));
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler that throws is reported as `internal`, on both transports.
|
||||
*
|
||||
* `RPCHandler` catches the throw and injects `internal`, so the `catch`
|
||||
* arms in `processSession` and `processRequest` are not what answers this.
|
||||
* What this pins is the reply: `ledger_entry` is `Role::USER`, so an
|
||||
* anonymous client reaches it.
|
||||
*
|
||||
* @param yield The coroutine the requests run on.
|
||||
*/
|
||||
void
|
||||
testInternalErrorIsReportedOnBothTransports(boost::asio::yield_context& yield)
|
||||
{
|
||||
testcase("An internal error is reported on both transports");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
static constexpr char const* kSensitiveSeed = "snoPBrXtMeMyMHUVTgbuqAfg1SUTb";
|
||||
|
||||
std::string logs;
|
||||
{
|
||||
Env env{
|
||||
*this, envconfig(), std::make_unique<CaptureLogs>(&logs), beast::Severity::Info};
|
||||
env.close();
|
||||
|
||||
auto const port = env.app().config()[Sections::kPortWs].get<std::uint16_t>(Keys::kPort);
|
||||
auto const ip = env.app().config()[Sections::kPortWs].get<std::string>(Keys::kIp);
|
||||
|
||||
json::Value params(json::ValueType::Object);
|
||||
params[jss::ledger_index] = jss::validated;
|
||||
params[jss::hashes] = -1;
|
||||
params[jss::secret] = kSensitiveSeed;
|
||||
|
||||
json::Value jv;
|
||||
jv[jss::method] = "ledger_entry";
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
jv[jss::params][0u] = params;
|
||||
|
||||
boost::system::error_code ec;
|
||||
Response resp;
|
||||
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv));
|
||||
BEAST_EXPECTS(reply[jss::result][jss::error] == "internal", to_string(reply));
|
||||
|
||||
json::Value frame(params);
|
||||
frame[jss::command] = "ledger_entry";
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
auto const wsReply = doWSMessage(yield, *ip, *port, to_string(frame));
|
||||
BEAST_EXPECTS(wsReply[jss::error] == "internal", to_string(wsReply));
|
||||
}
|
||||
|
||||
// The capture is not empty, so the absence assertion below is not vacuous.
|
||||
BEAST_EXPECT(logs.contains("Caught throw: "));
|
||||
|
||||
// The line reports what was thrown and never the request.
|
||||
BEAST_EXPECT(!logs.contains(kSensitiveSeed));
|
||||
}
|
||||
|
||||
/**
|
||||
* The credentials the command line client adds are masked in what it
|
||||
* prints.
|
||||
*
|
||||
* The client copies `admin_user` and `admin_password` out of `[port_rpc]`
|
||||
* into the request it builds, and prints to stdout rather than to a log.
|
||||
*
|
||||
* The account is asserted unmasked beside them, so that masking everything
|
||||
* does not satisfy this case.
|
||||
*
|
||||
* `request_sent` is a second echo of the same request and is not exercised
|
||||
* here: `fromCommandLine` writes it only on the transport-error path, which
|
||||
* no jtx harness drives.
|
||||
*/
|
||||
void
|
||||
testCommandLineCredentialsAreMasked()
|
||||
{
|
||||
testcase("The command line client masks the credentials it adds");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
static constexpr char const* kConfiguredPassword = "correct-horse-battery-staple";
|
||||
|
||||
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
(*cfg)[Sections::kPortRpc].set(Keys::kAdminUser, "operator");
|
||||
(*cfg)[Sections::kPortRpc].set(Keys::kAdminPassword, kConfiguredPassword);
|
||||
return cfg;
|
||||
})};
|
||||
|
||||
// `Env::doRpc` retries an internal error, repeating the request for no gain here.
|
||||
env.setRetries(0);
|
||||
|
||||
// Well formed, so the parser accepts it and the server answers `actNotFound`. A malformed
|
||||
// account fails in the parser, before there is a sent request to echo.
|
||||
auto const output = env.rpc("account_info", Account{"never-funded"}.human());
|
||||
|
||||
// The call reached the server, so the echo below is the request the client built.
|
||||
auto const& result = output[jss::result];
|
||||
BEAST_EXPECTS(result[jss::error] == "actNotFound", to_string(output));
|
||||
|
||||
// The credentials from the config are masked; the account the operator typed is not.
|
||||
BEAST_EXPECT(result[jss::request]["admin_password"] == "<masked>");
|
||||
BEAST_EXPECT(result[jss::request]["admin_user"] == "<masked>");
|
||||
BEAST_EXPECT(result[jss::request][jss::account] == Account{"never-funded"}.human());
|
||||
|
||||
// The subject of the fix: the configured password reaches no member of the output.
|
||||
BEAST_EXPECT(!to_string(output).contains(kConfiguredPassword));
|
||||
}
|
||||
|
||||
/**
|
||||
* A credential in an `[rpc_startup]` command is masked in the startup
|
||||
* log, and so is the result.
|
||||
*
|
||||
* Both lines are written at fatal, which the capture is taken at, and
|
||||
* only by a server that is not quiet. The passphrase is the marker for the
|
||||
* command line; a `wallet_propose` for a passphrase is deterministic, so
|
||||
* the `master_seed` the same command returns through the client is the
|
||||
* marker for the result line.
|
||||
*/
|
||||
void
|
||||
testStartupCommandsAreMasked()
|
||||
{
|
||||
testcase("Startup commands and their results are masked in the log");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
static constexpr char const* kPassphrase = "startup-passphrase-marker";
|
||||
static constexpr char const* kMalformedMarker = "startup-malformed-marker";
|
||||
|
||||
std::string logs;
|
||||
std::string seed;
|
||||
{
|
||||
Env env{
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
// The test config is quiet, and a quiet server writes neither line.
|
||||
cfg->setupControl(false, false, true);
|
||||
json::Value command(json::ValueType::Object);
|
||||
command[jss::command] = "wallet_propose";
|
||||
command[jss::passphrase] = kPassphrase;
|
||||
cfg->section(Sections::kRpcStartup).append(to_string(command));
|
||||
// A second line that does not parse (no closing quote or brace) and
|
||||
// carries a marker where a credential would be. Nothing parsed, so
|
||||
// masking cannot reach it; the line must not be echoed at all.
|
||||
cfg->section(Sections::kRpcStartup)
|
||||
.append(std::string{R"({"passphrase": ")"} + kMalformedMarker);
|
||||
return cfg;
|
||||
}),
|
||||
std::make_unique<CaptureLogs>(&logs),
|
||||
beast::Severity::Fatal};
|
||||
|
||||
json::Value params(json::ValueType::Object);
|
||||
params[jss::passphrase] = kPassphrase;
|
||||
auto const reply = env.rpc("json", "wallet_propose", to_string(params));
|
||||
seed = reply[jss::result][jss::master_seed].asString();
|
||||
}
|
||||
|
||||
// Both lines were written, so the absence assertions below are not vacuous.
|
||||
BEAST_EXPECT(logs.contains("Startup RPC: "));
|
||||
BEAST_EXPECT(logs.contains("Result: "));
|
||||
BEAST_EXPECT(logs.contains("<masked>"));
|
||||
|
||||
BEAST_EXPECT(!logs.contains(kPassphrase));
|
||||
BEAST_EXPECT(!seed.empty());
|
||||
BEAST_EXPECT(!logs.contains(seed));
|
||||
|
||||
// The malformed line is reported by its position and the parser's message, never by
|
||||
// its text, and no command runs for it: only the well-formed line was logged as run.
|
||||
BEAST_EXPECT(logs.contains("Couldn't parse entry 2 in [rpc_startup]"));
|
||||
BEAST_EXPECT(!logs.contains(kMalformedMarker));
|
||||
auto const occurrences = [&logs](std::string_view needle) {
|
||||
std::size_t count = 0;
|
||||
for (auto at = logs.find(needle); at != std::string::npos;
|
||||
at = logs.find(needle, at + 1))
|
||||
{
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
BEAST_EXPECT(occurrences("Startup RPC: ") == 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* No credential reaches the log at trace, on any transport.
|
||||
*
|
||||
* Several render sites write only at `trace`, and no other test raises
|
||||
* the threshold that far. A `wallet_propose` reply is the control for the
|
||||
* `HTTP Reply` line: it carries the keys it generated, and that line
|
||||
* carries the status only.
|
||||
*
|
||||
* @param yield The coroutine the requests run on.
|
||||
*/
|
||||
void
|
||||
testNoCredentialReachesTheLogAtTrace(boost::asio::yield_context& yield)
|
||||
{
|
||||
testcase("No credential reaches the log at trace");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
static constexpr char const* kSensitive = "sensitive-trace-value";
|
||||
|
||||
std::string logs;
|
||||
std::string seed;
|
||||
{
|
||||
Env env{
|
||||
*this, envconfig(), std::make_unique<CaptureLogs>(&logs), beast::Severity::Trace};
|
||||
|
||||
auto const port = env.app().config()[Sections::kPortWs].get<std::uint16_t>(Keys::kPort);
|
||||
auto const ip = env.app().config()[Sections::kPortWs].get<std::string>(Keys::kIp);
|
||||
|
||||
json::Value params(json::ValueType::Object);
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
params[std::string{field}] = kSensitive;
|
||||
|
||||
json::Value jv;
|
||||
jv[jss::method] = "ping";
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
jv[jss::params][0u] = params;
|
||||
|
||||
boost::system::error_code ec;
|
||||
Response resp;
|
||||
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv));
|
||||
BEAST_EXPECT(reply[jss::result][jss::status] == jss::success);
|
||||
|
||||
json::Value frame(params);
|
||||
frame[jss::command] = "ping";
|
||||
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
||||
doWSMessage(yield, *ip, *port, to_string(frame));
|
||||
|
||||
json::Value wallet;
|
||||
wallet[jss::method] = "wallet_propose";
|
||||
auto const proposed = postAndParse(env, yield, resp, ec, to_string(wallet));
|
||||
seed = proposed[jss::result][jss::master_seed].asString();
|
||||
}
|
||||
|
||||
// Both transports logged, so an empty capture cannot pass this.
|
||||
BEAST_EXPECT(logs.contains("Websocket received '"));
|
||||
BEAST_EXPECT(logs.contains("doRpcCommand:"));
|
||||
BEAST_EXPECT(logs.contains("HTTP Reply "));
|
||||
|
||||
BEAST_EXPECT(!logs.contains(kSensitive));
|
||||
BEAST_EXPECT(logs.contains("<masked>"));
|
||||
|
||||
// The seed the client received reaches no line, the status-only reply line included.
|
||||
BEAST_EXPECT(!seed.empty());
|
||||
BEAST_EXPECT(!logs.contains(seed));
|
||||
}
|
||||
|
||||
void
|
||||
testStatusNotOkay(boost::asio::yield_context& yield)
|
||||
{
|
||||
@@ -1180,8 +1843,17 @@ public:
|
||||
testNoRPC(yield);
|
||||
testWSRequests(yield);
|
||||
testRPCRequests(yield);
|
||||
testMaskedCredentials(yield);
|
||||
testTheLoggedRequestIsMaskedAndCapped(yield);
|
||||
testTheLoggedReplyIsMaskedOnlyWhenItCarriesACredential(yield);
|
||||
testInternalErrorIsReportedOnBothTransports(yield);
|
||||
testNoCredentialReachesTheLogAtTrace(yield);
|
||||
testGainedStatusesStayOffLegacyEnvelope(yield);
|
||||
testStatusNotOkay(yield);
|
||||
});
|
||||
|
||||
testCommandLineCredentialsAreMasked();
|
||||
testStartupCommandsAreMasked();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -598,6 +598,91 @@ TEST(JsonValue, compare_strings)
|
||||
#pragma pop_macro("DO_COMPARE")
|
||||
}
|
||||
|
||||
TEST(JsonValue, construct_from_string_view)
|
||||
{
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
// Characters are copied with an explicit length, so a view need not be NUL-terminated.
|
||||
EXPECT_EQ(json::Value{"2.0"sv}.asString(), "2.0");
|
||||
EXPECT_EQ(json::Value{"2.0 and more"sv.substr(0, 3)}.asString(), "2.0");
|
||||
EXPECT_EQ(json::Value{""sv}.asString(), "");
|
||||
|
||||
// A default-constructed view has a null `data()` and yields an empty string value.
|
||||
std::string_view const empty;
|
||||
json::Value const fromEmpty{empty};
|
||||
EXPECT_TRUE(fromEmpty.isString());
|
||||
EXPECT_EQ(fromEmpty.asString(), "");
|
||||
EXPECT_TRUE(fromEmpty == ""sv);
|
||||
|
||||
// The view need not outlive the call.
|
||||
json::Value copied;
|
||||
{
|
||||
std::string const owner{"borrowed"};
|
||||
copied = json::Value{std::string_view{owner}};
|
||||
}
|
||||
EXPECT_EQ(copied.asString(), "borrowed");
|
||||
|
||||
// A reader of a string value stops at the first NUL, so a view carrying one reads back short.
|
||||
json::Value const embedded{"a\0b"sv};
|
||||
EXPECT_EQ(embedded.asString(), "a");
|
||||
EXPECT_TRUE(embedded != "a\0b"sv);
|
||||
EXPECT_TRUE(embedded == "a"sv);
|
||||
EXPECT_TRUE(embedded == json::Value{"a\0b"sv});
|
||||
}
|
||||
|
||||
TEST(JsonValue, compare_string_view)
|
||||
{
|
||||
// Comparing against a view builds no temporary Value.
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
EXPECT_TRUE(json::Value{"2.0"} == "2.0"sv);
|
||||
EXPECT_FALSE(json::Value{"2.0"} != "2.0"sv);
|
||||
EXPECT_TRUE(json::Value{"2.0"} != "2.00"sv);
|
||||
EXPECT_TRUE(json::Value{"2.0"} != ""sv);
|
||||
|
||||
// A view need not be NUL-terminated, and the comparison must not read past its end.
|
||||
EXPECT_TRUE(json::Value{"2.0"} == "2.0 and more"sv.substr(0, 3));
|
||||
|
||||
EXPECT_TRUE(json::Value{""} == ""sv);
|
||||
|
||||
// A string value holding a null pointer equals nothing, not even the empty view.
|
||||
EXPECT_TRUE(json::Value{json::ValueType::String} != ""sv);
|
||||
EXPECT_TRUE(json::Value{json::ValueType::String} != "2.0"sv);
|
||||
EXPECT_TRUE(json::Value{json::ValueType::String} != json::Value{""});
|
||||
|
||||
// Nothing that is not a string equals one. Null is what an absent member reads as.
|
||||
EXPECT_TRUE(json::Value{json::ValueType::Null} != ""sv);
|
||||
EXPECT_TRUE(json::Value{json::ValueType::Null} != "2.0"sv);
|
||||
EXPECT_TRUE(json::Value{2} != "2"sv);
|
||||
EXPECT_TRUE(json::Value{true} != "true"sv);
|
||||
EXPECT_TRUE(json::Value{json::ValueType::Object} != ""sv);
|
||||
|
||||
// Only a view reaches this overload; every other spelling compares through Value.
|
||||
static constexpr json::StaticString kVersion{"2.0"};
|
||||
EXPECT_TRUE(json::Value{"2.0"} == kVersion);
|
||||
EXPECT_TRUE(json::Value{"2.1"} != kVersion);
|
||||
EXPECT_TRUE(json::Value{} != kVersion);
|
||||
|
||||
EXPECT_TRUE(json::Value{"2.0"} == "2.0");
|
||||
EXPECT_TRUE(json::Value{"2.0"} != "2.1");
|
||||
EXPECT_TRUE(json::Value{} != "2.0");
|
||||
|
||||
EXPECT_TRUE(json::Value{"2.0"} == std::string{"2.0"});
|
||||
EXPECT_TRUE(json::Value{"2.0"} != std::string{"2.1"});
|
||||
EXPECT_TRUE(json::Value{json::ValueType::Null} != std::string{"2.0"});
|
||||
|
||||
// A literal `0` also converts to a view through `char const*`, so a plain overload would make
|
||||
// this comparison ambiguous. It must compare as a number: RPCCall.cpp rejects a
|
||||
// `transaction_entry` ledger index this way.
|
||||
EXPECT_TRUE(json::Value{0} == 0);
|
||||
EXPECT_TRUE(json::Value{4294967295u} != 0);
|
||||
EXPECT_TRUE(json::Value{""} != 0);
|
||||
EXPECT_TRUE(json::Value{2} != 0);
|
||||
|
||||
// A null and an Int are different types, so the comparison is false.
|
||||
EXPECT_TRUE(json::Value{} != 0);
|
||||
}
|
||||
|
||||
TEST(JsonValue, bool)
|
||||
{
|
||||
EXPECT_FALSE(json::Value());
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <set>
|
||||
#include <type_traits>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
// A row cannot be written without a status, so no row can default to 200 again.
|
||||
static_assert(!std::is_constructible_v<rpc::ErrorInfo, ErrorCodeI, char const*, char const*>);
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* The numbers between `RpcBadSyntax` and `RpcLast` that have no row in the
|
||||
* error table: those no enumerator uses, and `RpcReportingUnsupported` (91),
|
||||
* the one enumerator the table does not list.
|
||||
*/
|
||||
std::set<int> const kGaps{
|
||||
5, 8, 20, 24, 25, 26, 27, 28, 34, 38, 39, 54, 55, 56,
|
||||
59, 60, 61, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ErrorCodes, table_gaps_report_the_unknown_placeholder)
|
||||
{
|
||||
// Codes absent from the table report the placeholder, so a scan by code must skip them.
|
||||
auto const& gap = rpc::getErrorInfo(static_cast<ErrorCodeI>(5));
|
||||
EXPECT_EQ(gap.code, RpcUnknown);
|
||||
EXPECT_STREQ(gap.token.cStr(), "unknown");
|
||||
|
||||
auto const& real = rpc::getErrorInfo(RpcInvalidParams);
|
||||
EXPECT_EQ(real.code, RpcInvalidParams);
|
||||
EXPECT_STREQ(real.token.cStr(), "invalidParams");
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, exactly_the_listed_codes_have_no_row)
|
||||
{
|
||||
// The gap set is written out by hand, so an enumerator added without a row fails here, where a
|
||||
// test asking the table which codes it knows would take the missing row for a gap.
|
||||
for (int code = RpcBadSyntax; code <= RpcLast; ++code)
|
||||
{
|
||||
bool const isGap = rpc::getErrorInfo(static_cast<ErrorCodeI>(code)).code == RpcUnknown;
|
||||
EXPECT_EQ(isGap, kGaps.contains(code)) << "code " << code;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, every_code_names_an_http_status)
|
||||
{
|
||||
// Written out here rather than read from the row: a test reading the row production reads
|
||||
// cannot tell whether the row is right.
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcActMalformed), 400);
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcActNotFound), 404);
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcAlreadyMultisig), 400);
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcAlreadySingleSig), 400);
|
||||
|
||||
// Every row names a status of its own. A code with no row reports the placeholder's, which the
|
||||
// default constructor fixes at 200.
|
||||
for (int code = RpcBadSyntax; code <= RpcLast; ++code)
|
||||
{
|
||||
auto const status = rpc::errorCodeHttpStatus(static_cast<ErrorCodeI>(code));
|
||||
if (kGaps.contains(code))
|
||||
{
|
||||
EXPECT_EQ(status, 200) << "code " << code;
|
||||
}
|
||||
else
|
||||
{
|
||||
EXPECT_NE(status, 200) << "code " << code;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,598 +0,0 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.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/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <protocol/VaultTestHelpers.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
namespace {
|
||||
|
||||
beast::Journal const kNullJournal{beast::Journal::getNullSink()};
|
||||
|
||||
// Snapshots every field adjustVaultBalances can touch, for atomicity checks.
|
||||
struct BalanceSnapshot
|
||||
{
|
||||
Number available;
|
||||
Number assetsDeployed;
|
||||
Number loss;
|
||||
Number yield;
|
||||
Number total;
|
||||
|
||||
static BalanceSnapshot
|
||||
of(SLE::ConstRef vault)
|
||||
{
|
||||
return {
|
||||
.available = vault->at(sfAssetsAvailable),
|
||||
.assetsDeployed = vault->at(sfAssetsDeployed),
|
||||
.loss = vault->at(sfLossUnrealized),
|
||||
.yield = vault->at(sfYieldUnrealized),
|
||||
.total = vault->at(sfAssetsTotal),
|
||||
};
|
||||
}
|
||||
|
||||
bool
|
||||
operator==(BalanceSnapshot const&) const = default;
|
||||
};
|
||||
|
||||
class VaultBalance : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
test::Account const issuer_{"issuer"};
|
||||
Issue const iou_{toCurrency("USD"), issuer_.id()};
|
||||
|
||||
// FixedPrecision IOU vault; stored is the (derived, hence possibly
|
||||
// stale) sfAssetsTotal cache.
|
||||
[[nodiscard]] std::shared_ptr<SLE>
|
||||
iouVault(
|
||||
Number const& assetsDeployed,
|
||||
Number const& available,
|
||||
Number const& stored = Number{0},
|
||||
std::uint8_t scale = 6) const
|
||||
{
|
||||
return makeVault(
|
||||
iou_, stored, VaultVersion::FixedPrecision, scale, assetsDeployed, available);
|
||||
}
|
||||
|
||||
// AssetsDeployed 500, available 100.
|
||||
[[nodiscard]] std::shared_ptr<SLE>
|
||||
standardVault() const
|
||||
{
|
||||
return iouVault(Number{500}, Number{100});
|
||||
}
|
||||
|
||||
[[nodiscard]] STAmount
|
||||
iouAmount(Number const& n) const
|
||||
{
|
||||
return STAmount{iou_, n};
|
||||
}
|
||||
|
||||
// Zero cash change: syncs the cached sfAssetsTotal.
|
||||
static void
|
||||
syncTotal(std::shared_ptr<SLE> const& vault)
|
||||
{
|
||||
ASSERT_EQ(adjustVaultBalances(vault, {}, kNullJournal), tesSUCCESS);
|
||||
}
|
||||
|
||||
// Above coarsening the cached sfAssetsTotal (Downward, 16 digits) is a
|
||||
// floor of the exact total: never above it, and within one unit of its
|
||||
// own live exponent.
|
||||
void
|
||||
expectCacheFloorsExactTotal(std::shared_ptr<SLE> const& vault) const
|
||||
{
|
||||
Number const exact = getAssetsTotal(vault);
|
||||
Number const cached = vault->at(sfAssetsTotal);
|
||||
EXPECT_LE(cached, exact);
|
||||
int const liveExponent = scale(cached, iou_);
|
||||
EXPECT_LE(exact - cached, (Number{1, liveExponent}));
|
||||
}
|
||||
|
||||
// Adjusts asset's integral vault by +25 and by an overdraft.
|
||||
static void
|
||||
checkIntegralControl(Asset const& asset)
|
||||
{
|
||||
auto const vault =
|
||||
makeVault(asset, Number{0}, VaultVersion::FixedPrecision, 0, Number{500}, Number{100});
|
||||
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = STAmount{asset, Number{25}}}, kNullJournal),
|
||||
tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{125});
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
|
||||
EXPECT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = STAmount{asset, Number{-1'000}}}, kNullJournal),
|
||||
tefBAD_LEDGER);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getAssetsTotal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(VaultBalance, get_assets_total_stored_for_non_fixed_precision)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
VaultVersion version = VaultVersion::Legacy;
|
||||
std::int64_t stored = 0;
|
||||
std::int64_t available = 0;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{.name = "Legacy", .version = VaultVersion::Legacy, .stored = 1'234'567, .available = 999},
|
||||
{
|
||||
.name = "CashBasis",
|
||||
.version = VaultVersion::CashBasis,
|
||||
.stored = 9'876'543,
|
||||
.available = 100,
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
auto const vault =
|
||||
makeVault(iou_, Number{row.stored}, row.version, 6, Number{0}, Number{row.available});
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{row.stored});
|
||||
EXPECT_FALSE(vault->isFieldPresent(sfAssetsDeployed));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, get_assets_total_fixed_precision_derived_sum)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
Number assetsDeployed;
|
||||
Number available;
|
||||
// The synced cache equals the exact total (not merely a floor of it).
|
||||
bool cacheExact = false;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{
|
||||
.name = "ExactSum",
|
||||
.assetsDeployed = Number{250'000},
|
||||
.available = Number{1'000'000},
|
||||
.cacheExact = true,
|
||||
},
|
||||
// available is already 16 significant digits; assetsDeployed pushes the exact
|
||||
// sum to 17, past what a 16-digit STAmount represents exactly.
|
||||
{
|
||||
.name = "ExactPastSixteenDigits",
|
||||
.assetsDeployed = Number{8, -6},
|
||||
.available = Number{9'999'999'999'999'999, -6},
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
auto const vault = iouVault(row.assetsDeployed, row.available);
|
||||
Number const exact = row.available + row.assetsDeployed;
|
||||
|
||||
// Exact sum: no rounding, no conversion to a 16-digit STAmount.
|
||||
EXPECT_EQ(getAssetsTotal(vault), exact);
|
||||
EXPECT_GE(getAssetsTotal(vault), vault->at(sfAssetsAvailable));
|
||||
|
||||
syncTotal(vault);
|
||||
if (row.cacheExact)
|
||||
{
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
}
|
||||
expectCacheFloorsExactTotal(vault);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, get_assets_total_fixed_precision_integral_exact)
|
||||
{
|
||||
auto const vault = makeVault(
|
||||
xrpIssue(), Number{0}, VaultVersion::FixedPrecision, 0, Number{250}, Number{1'000});
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{1'250});
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, get_assets_total_ignores_stale_cache)
|
||||
{
|
||||
Number const available{4'000'000};
|
||||
Number const assetsDeployed{1'000'000};
|
||||
auto const vault = iouVault(assetsDeployed, available, Number{99});
|
||||
EXPECT_EQ(getAssetsTotal(vault), available + assetsDeployed);
|
||||
EXPECT_NE(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
}
|
||||
|
||||
// The exact total, and the synced cache once Downward-rounded, must never
|
||||
// fall below AssetsAvailable even when assetsDeployed coarsens the scale.
|
||||
TEST_F(VaultBalance, get_assets_total_never_below_available)
|
||||
{
|
||||
Number const available{9'999'999'999'999'999, -6};
|
||||
Number const assetsDeployed{5'000'000'000, -6};
|
||||
auto const vault = iouVault(assetsDeployed, available);
|
||||
EXPECT_GT(getVaultScale(vault), getVaultBaseScale(vault));
|
||||
EXPECT_GE(getAssetsTotal(vault), available);
|
||||
|
||||
syncTotal(vault);
|
||||
EXPECT_GE(Number(vault->at(sfAssetsTotal)), available);
|
||||
}
|
||||
|
||||
// available and assetsDeployed are each at most 16 digits, so associateAsset leaves them
|
||||
// alone. Their sum, 99999999999999995 at exponent -4, has a 17th digit (5)
|
||||
// that would round up under ToNearest and carry through the run of nines,
|
||||
// bumping the exponent. Downward truncation drops it without a bump.
|
||||
TEST_F(VaultBalance, live_scale_downward_keeps_lower_exponent_at_carry_boundary)
|
||||
{
|
||||
auto vault = iouVault(Number{35, -4}, Number{9'999'999'999'999'996LL, -3});
|
||||
|
||||
int const downwardExponent = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return scale(getAssetsTotal(vault), iou_);
|
||||
}();
|
||||
int const toNearestExponent = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
|
||||
return scale(getAssetsTotal(vault), iou_);
|
||||
}();
|
||||
EXPECT_LT(downwardExponent, toNearestExponent);
|
||||
EXPECT_EQ(getVaultScale(vault), downwardExponent);
|
||||
|
||||
syncTotal(vault);
|
||||
EXPECT_EQ(scale(vault->at(sfAssetsTotal), iou_), downwardExponent);
|
||||
EXPECT_EQ(getVaultScale(vault), scale(vault->at(sfAssetsTotal), iou_));
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, scale_and_open_include_assets_deployed)
|
||||
{
|
||||
// Available alone stays at base scale; Available + AssetsDeployed coarsens.
|
||||
Number const available{1'000'000'000};
|
||||
Number const assetsDeployed{9'000'000'000};
|
||||
auto const vault = iouVault(assetsDeployed, available, available);
|
||||
|
||||
EXPECT_EQ(getAssetsTotal(vault), available + assetsDeployed);
|
||||
EXPECT_GT(getVaultScale(vault), getVaultBaseScale(vault));
|
||||
|
||||
// Open capacity uses the derived total, so assetsDeployed counts against the
|
||||
// ceiling: with assetsDeployed filling most of Open, the inflow is rejected.
|
||||
auto const nearOpen = iouVault(Number{1, 9}, Number{8, 9});
|
||||
EXPECT_EQ(checkOptionalVaultInflow(nearOpen, iouAmount(Number{1})), tecLIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// adjustVaultBalances
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(VaultBalance, fixed_precision_adjust_balances)
|
||||
{
|
||||
Number const assetsDeployed{300};
|
||||
auto const vault = iouVault(assetsDeployed, Number{100}, Number{99});
|
||||
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = iouAmount(Number{50})}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{150});
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), assetsDeployed);
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{450});
|
||||
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = iouAmount(Number{-20})}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{130});
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), assetsDeployed);
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{430});
|
||||
|
||||
// Clear both rails together, the way a final VaultWithdraw does.
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(
|
||||
vault,
|
||||
{.cash = iouAmount(Number{-130}), .deployed = -Number(assetsDeployed)},
|
||||
kNullJournal),
|
||||
tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{0});
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), Number{0});
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), Number{0});
|
||||
EXPECT_EQ(getAssetsTotal(vault), Number{0});
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, fixed_precision_add_above_coarsening_syncs)
|
||||
{
|
||||
Number const assetsDeployed{5, -6};
|
||||
auto const vault = iouVault(assetsDeployed, Number{9'999'999'999'999'990, -6});
|
||||
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(vault, {.cash = iouAmount(Number{10, -6})}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), assetsDeployed);
|
||||
expectCacheFloorsExactTotal(vault);
|
||||
}
|
||||
|
||||
// For each (balance, delta), AssetsAvailable after adjustVaultBalances must
|
||||
// equal the plain STAmount sum, the arithmetic the ledger uses for the real
|
||||
// trust-line/MPT transfer.
|
||||
TEST_F(VaultBalance, trust_line_arithmetic_mirror)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
Number balance;
|
||||
Number delta;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{.name = "Plain", .balance = Number{1'000'000}, .delta = Number{500'000}},
|
||||
{
|
||||
.name = "CreditCrossesPowerOfTenUp",
|
||||
.balance = Number{9'999'999'999'999'999, -9},
|
||||
.delta = Number{2, -9},
|
||||
},
|
||||
{
|
||||
.name = "DebitCrossesPowerOfTenDown",
|
||||
.balance = Number{1'000'000'000'000'000, -9},
|
||||
.delta = Number{-1, -9},
|
||||
},
|
||||
{.name = "DebitPartial", .balance = Number{5'000'000}, .delta = Number{-3'000'000}},
|
||||
{.name = "DebitToZero", .balance = Number{2'000'000}, .delta = Number{-2'000'000}},
|
||||
{
|
||||
.name = "CreditFinerThanBalanceGrid",
|
||||
.balance = Number{1'873'013'129'122'272LL, -9},
|
||||
.delta = Number{7, -10},
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
STAmount const balance{iou_, row.balance};
|
||||
STAmount const delta{iou_, row.delta};
|
||||
STAmount const expected{iou_, Number(balance) + Number(delta)};
|
||||
|
||||
auto const vault = iouVault(Number{0}, row.balance, Number{0}, 9);
|
||||
ASSERT_EQ(adjustVaultBalances(vault, {.cash = delta}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(Number(vault->at(sfAssetsAvailable)), Number(expected));
|
||||
}
|
||||
}
|
||||
|
||||
// Each row starts from assetsDeployed 500, available 100.
|
||||
TEST_F(VaultBalance, fields_move)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
std::int64_t cash = 0;
|
||||
std::int64_t deployed = 0;
|
||||
std::int64_t yield = 0;
|
||||
std::int64_t loss = 0;
|
||||
std::int64_t expectedAvailable = 100;
|
||||
std::int64_t expectedAssetsDeployed = 500;
|
||||
std::int64_t expectedYield = 0;
|
||||
std::int64_t expectedLoss = 0;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{.name = "CashAlone", .cash = 25, .expectedAvailable = 125},
|
||||
{.name = "AssetsDeployedAlone", .deployed = -200, .expectedAssetsDeployed = 300},
|
||||
{.name = "YieldAlone", .yield = 40, .expectedYield = 40},
|
||||
{.name = "LossAlone", .loss = 150, .expectedLoss = 150},
|
||||
{
|
||||
.name = "AllTogether",
|
||||
.cash = 40,
|
||||
.deployed = -100,
|
||||
.yield = 10,
|
||||
.loss = 50,
|
||||
.expectedAvailable = 140,
|
||||
.expectedAssetsDeployed = 400,
|
||||
.expectedYield = 10,
|
||||
.expectedLoss = 50,
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
auto const vault = standardVault();
|
||||
ASSERT_EQ(
|
||||
adjustVaultBalances(
|
||||
vault,
|
||||
{.cash = iouAmount(Number{row.cash}),
|
||||
.deployed = Number{row.deployed},
|
||||
.yield = Number{row.yield},
|
||||
.loss = Number{row.loss}},
|
||||
kNullJournal),
|
||||
tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfAssetsAvailable), Number{row.expectedAvailable});
|
||||
EXPECT_EQ(vault->at(sfAssetsDeployed), Number{row.expectedAssetsDeployed});
|
||||
EXPECT_EQ(vault->at(sfYieldUnrealized), Number{row.expectedYield});
|
||||
EXPECT_EQ(vault->at(sfLossUnrealized), Number{row.expectedLoss});
|
||||
EXPECT_EQ(vault->at(sfAssetsTotal), getAssetsTotal(vault));
|
||||
}
|
||||
}
|
||||
|
||||
// A failing field rejects the whole change as a unit, including any valid
|
||||
// cash leg. Each row starts from assetsDeployed 500, available 100.
|
||||
TEST_F(VaultBalance, invalid_change_fails_atomically)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
std::string_view name;
|
||||
std::int64_t cash = 0;
|
||||
std::int64_t deployed = 0;
|
||||
std::int64_t loss = 0;
|
||||
TER expected;
|
||||
};
|
||||
std::vector<Row> const rows{
|
||||
{.name = "AssetsAvailableNegative", .cash = -200, .expected = tefBAD_LEDGER},
|
||||
{.name = "AssetsDeployedNegative", .deployed = -600, .expected = tefBAD_LEDGER},
|
||||
// The +50 credit is valid on its own; the deployed change is not.
|
||||
{
|
||||
.name = "AssetsDeployedNegativeWithValidCash",
|
||||
.cash = 50,
|
||||
.deployed = -600,
|
||||
.expected = tefBAD_LEDGER,
|
||||
},
|
||||
// The -50 debit is valid on its own (available stays at 50); the
|
||||
// loss exceeds AssetsDeployed.
|
||||
{
|
||||
.name = "LossExceedsAssetsDeployedWithValidCash",
|
||||
.cash = -50,
|
||||
.loss = 600,
|
||||
.expected = tecLIMIT_EXCEEDED,
|
||||
},
|
||||
{.name = "LossUnrealizedNegative", .loss = -1, .expected = tefBAD_LEDGER},
|
||||
{.name = "LossExceedsAssetsDeployed", .loss = 600, .expected = tecLIMIT_EXCEEDED},
|
||||
};
|
||||
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
SCOPED_TRACE(row.name);
|
||||
auto const vault = standardVault();
|
||||
BalanceSnapshot const before = BalanceSnapshot::of(vault);
|
||||
|
||||
EXPECT_EQ(
|
||||
adjustVaultBalances(
|
||||
vault,
|
||||
{.cash = iouAmount(Number{row.cash}),
|
||||
.deployed = Number{row.deployed},
|
||||
.loss = Number{row.loss}},
|
||||
kNullJournal),
|
||||
row.expected);
|
||||
EXPECT_EQ(BalanceSnapshot::of(vault), before);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, loss_at_assets_deployed_boundary_allowed)
|
||||
{
|
||||
auto const vault = standardVault();
|
||||
EXPECT_EQ(adjustVaultBalances(vault, {.loss = Number{500}}, kNullJournal), tesSUCCESS);
|
||||
EXPECT_EQ(vault->at(sfLossUnrealized), Number{500});
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, negative_yield_unrealized_rejected)
|
||||
{
|
||||
auto const vault = standardVault();
|
||||
vault->at(sfYieldUnrealized) = Number{5};
|
||||
auto const before = BalanceSnapshot::of(vault);
|
||||
|
||||
EXPECT_EQ(adjustVaultBalances(vault, {.yield = Number{-20}}, kNullJournal), tefBAD_LEDGER);
|
||||
EXPECT_EQ(BalanceSnapshot::of(vault), before);
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, assets_total_equals_floor16_of_available_plus_assets_deployed)
|
||||
{
|
||||
Number const assetsDeployed{7, -9};
|
||||
auto vault = iouVault(assetsDeployed, Number{9'999'999'999'999'999, -9}, Number{0}, 9);
|
||||
|
||||
syncTotal(vault);
|
||||
|
||||
Number const expected = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
return Number(STAmount{iou_, Number(vault->at(sfAssetsAvailable)) + assetsDeployed});
|
||||
}();
|
||||
EXPECT_EQ(Number(vault->at(sfAssetsTotal)), expected);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integral assets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(VaultBalance, xrp_control)
|
||||
{
|
||||
checkIntegralControl(xrpIssue());
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, mpt_control)
|
||||
{
|
||||
checkIntegralControl(MPTIssue{makeMptID(1, issuer_.id())});
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_scale_ignores_ambient_rounding_mode)
|
||||
{
|
||||
// reference is on a fine grid (scale -10); raw needs a coarser atScale once
|
||||
// reference + raw is floored, so flooredSum - reference needs more digits
|
||||
// than fit in a 16-digit STAmount. creditToPosteriorScale must build that
|
||||
// difference under its own roundingMode (Downward here), not whatever
|
||||
// rounding mode happens to be ambient when it is called.
|
||||
Number const reference{4'218'667'505'995'833LL, -10};
|
||||
STAmount const raw{iou_, std::int64_t{4'004'426'170'440'612LL}, -8};
|
||||
int const atScale = -7;
|
||||
|
||||
// The exact (pre-STAmount) Downward credit, computed independently, floors
|
||||
// strictly below raw: part of raw is lost rounding reference + raw onto
|
||||
// the coarser atScale grid.
|
||||
Number const exactDownwardCredit = [&] {
|
||||
NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
|
||||
Number const flooredSum =
|
||||
roundToAsset(iou_, reference + Number(raw), atScale, Number::RoundingMode::Downward);
|
||||
return Number(STAmount{iou_, flooredSum - reference});
|
||||
}();
|
||||
EXPECT_LT(exactDownwardCredit, Number(raw));
|
||||
|
||||
// Calling under an ambient ToNearest mode (the common case: no caller sets
|
||||
// an explicit guard around this call) must still produce the Downward
|
||||
// result, not a value rounded under the ambient mode.
|
||||
NumberRoundModeGuard const ambient(Number::RoundingMode::ToNearest);
|
||||
STAmount const credit = detail::creditToPosteriorScale(
|
||||
iou_, reference, atScale, raw, Number::RoundingMode::Downward);
|
||||
EXPECT_LT(Number(credit), Number(raw));
|
||||
EXPECT_EQ(Number(credit), exactDownwardCredit);
|
||||
}
|
||||
|
||||
// creditToPosteriorAvailableScale returns
|
||||
// floor16(AssetsAvailable + raw) - AssetsAvailable, never finer than -Scale.
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_available_scale_integral_passes_through)
|
||||
{
|
||||
auto const vault = makeVault(xrpIssue(), Number{1'000}, VaultVersion::FixedPrecision);
|
||||
STAmount const credit =
|
||||
creditToPosteriorAvailableScale(vault, Number{250}, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, STAmount(xrpIssue(), 250));
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_available_scale_floors_to_base_grid)
|
||||
{
|
||||
// Scale 6: the base grid is 1e-6, and AssetsAvailable is small enough
|
||||
// that the posterior grid is the base grid.
|
||||
auto const vault = iouVault(Number{0}, Number{100});
|
||||
STAmount const credit = creditToPosteriorAvailableScale(
|
||||
vault, Number{12'345'678, -7}, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, iouAmount(Number{1'234'567, -6}));
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_available_scale_floors_the_sum_across_power_of_ten)
|
||||
{
|
||||
// 9999999999.999999 + 0.000011 crosses 10^10, where the 16-digit grid is
|
||||
// 1e-5. Flooring the delta alone would give 0.00001; flooring the sum
|
||||
// gives 10000000000.00001, so the credit is exactly 0.000011.
|
||||
auto const vault = iouVault(Number{0}, Number{9'999'999'999'999'999, -6});
|
||||
STAmount const credit =
|
||||
creditToPosteriorAvailableScale(vault, Number{11, -6}, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, iouAmount(Number{11, -6}));
|
||||
EXPECT_EQ(
|
||||
STAmount(iou_, Number{9'999'999'999'999'999, -6} + Number(credit)),
|
||||
iouAmount(Number{1'000'000'000'000'001, -5}));
|
||||
}
|
||||
|
||||
TEST_F(VaultBalance, credit_to_posterior_available_scale_uses_the_exact_raw_amount)
|
||||
{
|
||||
// raw has 17 significant digits. Exact: 0.000009 + 12345678901.234561 =
|
||||
// 12345678901.23457 on the 1e-5 grid, so the credit is
|
||||
// 12345678901.234561, stored as 12345678901.23456. Rounding raw to 16
|
||||
// digits first (12345678901.23456) would floor the sum to
|
||||
// 12345678901.23456 and give a credit one unit lower, 12345678901.23455.
|
||||
auto const vault = iouVault(Number{0}, Number{9, -6});
|
||||
STAmount const credit = creditToPosteriorAvailableScale(
|
||||
vault, Number{12'345'678'901'234'561, -6}, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, iouAmount(Number{1'234'567'890'123'456, -5}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl
|
||||
@@ -1,376 +0,0 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/hash/uhash.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/helpers/VaultHelpers.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rules.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>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <helpers/Account.h>
|
||||
#include <protocol/VaultTestHelpers.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace xrpl {
|
||||
namespace {
|
||||
|
||||
TEST(VaultGrid, base_and_live_scale)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
|
||||
auto const legacy = makeVault(iou, Number{1'000'000}, VaultVersion::Legacy, 6);
|
||||
EXPECT_EQ(getVaultScale(legacy), -9);
|
||||
EXPECT_EQ(getVaultBaseScale(legacy), getVaultScale(legacy));
|
||||
|
||||
auto const empty = makeVault(iou, Number{0}, VaultVersion::FixedPrecision, 6);
|
||||
EXPECT_EQ(getVaultScale(empty), -6);
|
||||
EXPECT_EQ(getVaultBaseScale(empty), -6);
|
||||
|
||||
auto const small = makeVault(iou, Number{1'000'000}, VaultVersion::FixedPrecision, 6);
|
||||
EXPECT_EQ(getVaultScale(small), -6);
|
||||
EXPECT_EQ(getVaultBaseScale(small), -6);
|
||||
|
||||
auto const coarsened = makeVault(iou, Number{10'000'000'000}, VaultVersion::FixedPrecision, 6);
|
||||
EXPECT_GT(getVaultScale(coarsened), getVaultBaseScale(coarsened));
|
||||
EXPECT_EQ(getVaultBaseScale(coarsened), -6);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, pre_v12_behavior_is_preserved)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
Number const assetsTotal{1'000'000};
|
||||
STAmount const onGrid{iou, Number{2, -9}};
|
||||
STAmount const dust{iou, Number{4, -10}};
|
||||
STAmount const overOpen{iou, Number{10, 9}};
|
||||
auto const downward = Number::RoundingMode::Downward;
|
||||
|
||||
auto const legacy = makeVault(iou, assetsTotal, VaultVersion::Legacy);
|
||||
int const expectedLive = getVaultScale(legacy);
|
||||
int const expectedPosterior = detail::getPosteriorVaultScale(legacy, onGrid);
|
||||
STAmount const expectedPosteriorRound = roundToPosteriorVaultScale(legacy, dust, downward);
|
||||
|
||||
for (auto const version : {
|
||||
std::optional<VaultVersion>{},
|
||||
std::optional{VaultVersion::Legacy},
|
||||
std::optional{VaultVersion::CashBasis},
|
||||
})
|
||||
{
|
||||
auto const vault = makeVault(iou, assetsTotal, version);
|
||||
EXPECT_EQ(getVaultScale(vault), expectedLive);
|
||||
EXPECT_EQ(getVaultBaseScale(vault), expectedLive);
|
||||
EXPECT_EQ(detail::getPosteriorVaultScale(vault, onGrid), expectedPosterior);
|
||||
EXPECT_EQ(roundToPosteriorVaultScale(vault, dust, downward), expectedPosteriorRound);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(vault, overOpen), tesSUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VaultGrid, pre_v12_credit_clamp_floors_posterior_total)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
Number const assetsTotal{9'999'999'999'999'999LL, -15};
|
||||
STAmount const delta{iou, Number{5}};
|
||||
STAmount const expected{iou, Number{4'999'999'999'999'991LL, -15}};
|
||||
|
||||
for (auto const version : {
|
||||
std::optional<VaultVersion>{},
|
||||
std::optional{VaultVersion::Legacy},
|
||||
std::optional{VaultVersion::CashBasis},
|
||||
})
|
||||
{
|
||||
auto const result = clampToAssetsTotalScale(makeVault(iou, assetsTotal, version), delta);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VaultGrid, integral_scale_is_zero)
|
||||
{
|
||||
auto const vault = makeVault(xrpIssue(), Number{1'000}, VaultVersion::FixedPrecision, 0);
|
||||
STAmount const delta{xrpIssue(), 7};
|
||||
|
||||
EXPECT_EQ(getVaultScale(vault), 0);
|
||||
EXPECT_EQ(getVaultBaseScale(vault), 0);
|
||||
EXPECT_EQ(detail::getPosteriorVaultScale(vault, delta), 0);
|
||||
EXPECT_EQ(roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero), delta);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, posterior_scale_rounds_delta)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault =
|
||||
makeVault(iou, Number{9'999'999'999'999'999, -6}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const delta{iou, Number{21, -6}};
|
||||
|
||||
EXPECT_EQ(getVaultScale(vault), -6);
|
||||
EXPECT_EQ(detail::getPosteriorVaultScale(vault, delta), -5);
|
||||
EXPECT_EQ(
|
||||
roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero),
|
||||
STAmount(iou, Number{20, -6}));
|
||||
}
|
||||
|
||||
TEST(VaultGrid, posterior_scale_rejects_dust_at_call_site)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault = makeVault(iou, Number{10'000'000'000}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const dust{iou, Number{1, -6}};
|
||||
|
||||
EXPECT_EQ(
|
||||
roundToPosteriorVaultScale(vault, dust, Number::RoundingMode::TowardsZero), beast::kZero);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, posterior_outflow_can_refine_scale)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault =
|
||||
makeVault(iou, Number{1'000'000'000'000'001, -5}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const delta{iou, -Number{11, -6}};
|
||||
|
||||
EXPECT_EQ(getVaultScale(vault), -5);
|
||||
EXPECT_EQ(detail::getPosteriorVaultScale(vault, delta), -6);
|
||||
EXPECT_EQ(roundToPosteriorVaultScale(vault, delta, Number::RoundingMode::TowardsZero), delta);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, optional_inflow_capacity_boundaries)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
|
||||
auto const fixedIou = makeVault(iou, Number{9, 5}, VaultVersion::FixedPrecision, 10);
|
||||
STAmount const iouDust{iou, Number{1, -10}};
|
||||
EXPECT_EQ(getVaultOpenLimit(fixedIou), (Number{9, 5}));
|
||||
EXPECT_EQ(checkOptionalVaultInflow(fixedIou, STAmount{iou}), tesSUCCESS);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(fixedIou, iouDust), tecLIMIT_EXCEEDED);
|
||||
|
||||
auto const fixedXrp = makeVault(xrpIssue(), Number{9, 15}, VaultVersion::FixedPrecision, 0);
|
||||
STAmount const xrpUnit{xrpIssue(), 1};
|
||||
EXPECT_EQ(getVaultOpenLimit(fixedXrp), (Number{9, 15}));
|
||||
EXPECT_EQ(checkOptionalVaultInflow(fixedXrp, STAmount{xrpIssue()}), tesSUCCESS);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(fixedXrp, xrpUnit), tecLIMIT_EXCEEDED);
|
||||
|
||||
auto const legacy = makeVault(iou, Number{10, 5}, VaultVersion::Legacy, 10);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(legacy, iouDust), tesSUCCESS);
|
||||
|
||||
auto const coarsening =
|
||||
makeVault(iou, Number{9'999'999'999'999'999, -6}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const coarseningDelta{iou, Number{21, -6}};
|
||||
EXPECT_EQ(checkOptionalVaultInflow(coarsening, coarseningDelta), tecLIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, optional_inflow_includes_yield_unrealized)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault = makeVault(iou, Number{8'999'999'999}, VaultVersion::FixedPrecision, 6);
|
||||
STAmount const amount{iou, Number{1}};
|
||||
|
||||
EXPECT_EQ(checkOptionalVaultInflow(vault, amount), tesSUCCESS);
|
||||
|
||||
vault->at(sfYieldUnrealized) = Number{1};
|
||||
associateAsset(*vault, iou);
|
||||
EXPECT_EQ(checkOptionalVaultInflow(vault, amount), tecLIMIT_EXCEEDED);
|
||||
}
|
||||
|
||||
// AssetsAvailable + raw needs 20 digits. Under the ToNearest ambient mode the
|
||||
// sum would round up to the next 10^-6 before the Downward floor, crediting
|
||||
// 0.000001 for a raw 0.0000009996.
|
||||
TEST(VaultGrid, credit_floors_sum_in_requested_mode)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
auto const vault = makeVault(
|
||||
iou,
|
||||
Number{0},
|
||||
VaultVersion::FixedPrecision,
|
||||
6,
|
||||
Number{0},
|
||||
Number{1'234'567'890'123'456LL, -6});
|
||||
Number const raw{9'996, -10};
|
||||
|
||||
NumberRoundModeGuard const ambient(Number::RoundingMode::ToNearest);
|
||||
STAmount const credit =
|
||||
creditToPosteriorAvailableScale(vault, raw, Number::RoundingMode::Downward);
|
||||
EXPECT_EQ(credit, STAmount(iou, 0));
|
||||
EXPECT_LE(Number(credit), raw);
|
||||
}
|
||||
|
||||
TEST(VaultGrid, is_on_vault_base_grid)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
int const baseScale = vaultBaseScale(iou, 6);
|
||||
|
||||
struct Row
|
||||
{
|
||||
Number value;
|
||||
bool expected{};
|
||||
};
|
||||
Row const rows[] = {
|
||||
{.value = Number{0}, .expected = true},
|
||||
{.value = Number{1}, .expected = true},
|
||||
{.value = Number{1, -6}, .expected = true},
|
||||
{.value = Number{1, 20}, .expected = true},
|
||||
{.value = Number{1'234'567'890'123'456LL, -6}, .expected = true},
|
||||
// A digit below baseScale.
|
||||
{.value = Number{1, -7}, .expected = false},
|
||||
{.value = Number{15, -7}, .expected = false},
|
||||
// A 17th significant digit, on the base grid but not an STAmount.
|
||||
{.value = Number{12'345'678'901'234'567LL, 0}, .expected = false},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
EXPECT_EQ(isOnVaultBaseGrid(iou, row.value, baseScale), row.expected) << row.value;
|
||||
|
||||
EXPECT_EQ(vaultBaseScale(xrpIssue(), 6), 0);
|
||||
EXPECT_TRUE(isOnVaultBaseGrid(xrpIssue(), Number{7}, 0));
|
||||
|
||||
// Out of range for the asset: rejected instead of throwing.
|
||||
EXPECT_FALSE(isOnVaultBaseGrid(
|
||||
xrpIssue(), Number{static_cast<std::int64_t>(STAmount::kMaxNativeN)} + 1, 0));
|
||||
EXPECT_FALSE(isOnVaultBaseGrid(iou, Number{1, 100}, baseScale));
|
||||
}
|
||||
|
||||
TEST(VaultGrid, clamp_vault_outflow)
|
||||
{
|
||||
test::Account const issuer{"issuer"};
|
||||
Issue const iou{toCurrency("USD"), issuer.id()};
|
||||
Asset const mpt{MPTIssue{makeMptID(1, issuer.id())}};
|
||||
|
||||
struct Row
|
||||
{
|
||||
char const* name = nullptr;
|
||||
Asset asset;
|
||||
Number available;
|
||||
Number delta;
|
||||
std::optional<Number> expected; // nullopt: tecPRECISION_LOSS
|
||||
};
|
||||
Row const rows[] = {
|
||||
{
|
||||
.name = "XRP outflow is returned unrounded",
|
||||
.asset = xrpIssue(),
|
||||
.available = Number{1'000},
|
||||
.delta = Number{-7},
|
||||
.expected = Number{7},
|
||||
},
|
||||
{
|
||||
.name = "MPT outflow is returned unrounded",
|
||||
.asset = mpt,
|
||||
.available = Number{1'000},
|
||||
.delta = Number{-7},
|
||||
.expected = Number{7},
|
||||
},
|
||||
{
|
||||
.name = "IOU outflow rounds towards zero at the posterior scale",
|
||||
.asset = iou,
|
||||
.available = Number{1'000'000},
|
||||
.delta = Number{-15, -7},
|
||||
.expected = Number{1, -6},
|
||||
},
|
||||
{
|
||||
.name = "IOU outflow below one unit at the posterior scale is rejected",
|
||||
.asset = iou,
|
||||
.available = Number{1'000'000},
|
||||
.delta = Number{-4, -7},
|
||||
.expected = std::nullopt,
|
||||
},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
auto const vault = makeVault(
|
||||
row.asset,
|
||||
row.available,
|
||||
VaultVersion::FixedPrecision,
|
||||
row.asset.integral() ? 0 : 6,
|
||||
Number{0},
|
||||
row.available);
|
||||
auto const result = clampVaultOutflow(vault, STAmount{row.asset, row.delta});
|
||||
if (row.expected)
|
||||
{
|
||||
ASSERT_TRUE(result.has_value()) << row.name;
|
||||
EXPECT_EQ(*result, (STAmount{row.asset, *row.expected})) << row.name;
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT_FALSE(result.has_value()) << row.name;
|
||||
EXPECT_EQ(result.error(), tecPRECISION_LOSS) << row.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(VaultGrid, get_vault_default_version)
|
||||
{
|
||||
struct Row
|
||||
{
|
||||
char const* name = nullptr;
|
||||
std::unordered_set<UInt256, beast::Uhash<>> enabled;
|
||||
VaultVersion expected;
|
||||
};
|
||||
Row const rows[] = {
|
||||
{.name = "no lending amendments: Legacy", .enabled = {}, .expected = VaultVersion::Legacy},
|
||||
{
|
||||
.name = "LendingProtocolV1_1: CashBasis",
|
||||
.enabled = {featureLendingProtocolV1_1},
|
||||
.expected = VaultVersion::CashBasis,
|
||||
},
|
||||
{
|
||||
.name = "V1_1 and V1_2 without the cleanups: CashBasis",
|
||||
.enabled = {featureLendingProtocolV1_1, featureLendingProtocolV1_2},
|
||||
.expected = VaultVersion::CashBasis,
|
||||
},
|
||||
{
|
||||
.name = "V1_1, V1_2 and fixCleanup3_2_0 without fixCleanup3_4_0: CashBasis",
|
||||
.enabled = {featureLendingProtocolV1_1, featureLendingProtocolV1_2, fixCleanup3_2_0},
|
||||
.expected = VaultVersion::CashBasis,
|
||||
},
|
||||
{
|
||||
.name = "V1_1, V1_2 and fixCleanup3_4_0 without fixCleanup3_2_0: CashBasis",
|
||||
.enabled = {featureLendingProtocolV1_1, featureLendingProtocolV1_2, fixCleanup3_4_0},
|
||||
.expected = VaultVersion::CashBasis,
|
||||
},
|
||||
{
|
||||
.name = "V1_2 and both cleanups: FixedPrecision",
|
||||
.enabled = {featureLendingProtocolV1_2, fixCleanup3_2_0, fixCleanup3_4_0},
|
||||
.expected = VaultVersion::FixedPrecision,
|
||||
},
|
||||
{
|
||||
.name = "V1_1, V1_2 and both cleanups: FixedPrecision",
|
||||
.enabled =
|
||||
{
|
||||
featureLendingProtocolV1_1,
|
||||
featureLendingProtocolV1_2,
|
||||
fixCleanup3_2_0,
|
||||
fixCleanup3_4_0,
|
||||
},
|
||||
.expected = VaultVersion::FixedPrecision,
|
||||
},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
// Rules keeps a reference to the preset set; row.enabled outlives it.
|
||||
Rules const rules{row.enabled};
|
||||
EXPECT_EQ(getVaultDefaultVersion(rules), row.expected) << row.name;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace xrpl
|
||||
@@ -1,65 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
// Needed for standalone compilation of sfScale/sfLEVersion assignment below;
|
||||
// full-TU builds already see it transitively, so misc-include-cleaner flags
|
||||
// it as unused there. Keep it so this header still compiles on its own.
|
||||
#include <xrpl/protocol/STInteger.h> // NOLINT(misc-include-cleaner)
|
||||
#include <xrpl/protocol/STIssue.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Shared by VaultGridTests, VaultBalanceTests and LoanBrokerCoverTests. For
|
||||
// FixedPrecision, getAssetsTotal derives from AssetsAvailable plus
|
||||
// AssetsDeployed rather than reading the stored AssetsTotal cache, so a
|
||||
// FixedPrecision vault must have AssetsAvailable (and, if requested, a
|
||||
// non-zero AssetsDeployed) seeded separately from assetsTotal. By default
|
||||
// AssetsAvailable is assetsTotal - assetsDeployed, so the derived total
|
||||
// equals assetsTotal; pass assetsAvailable explicitly to decouple it (e.g.
|
||||
// to leave assetsTotal as a deliberately stale cache). Legacy/CashBasis
|
||||
// vaults have no AssetsDeployed field and normally leave AssetsAvailable
|
||||
// unset, but some cash-flow tests need it seeded too, so assetsAvailable is
|
||||
// honored for those versions as well when given.
|
||||
inline std::shared_ptr<SLE>
|
||||
makeVault(
|
||||
Asset const& asset,
|
||||
Number const& assetsTotal,
|
||||
std::optional<VaultVersion> version,
|
||||
std::uint8_t scaleValue = kVaultDefaultIouScale,
|
||||
Number const& assetsDeployed = Number{0},
|
||||
std::optional<Number> assetsAvailable = std::nullopt)
|
||||
{
|
||||
auto vault = std::make_shared<SLE>(keylet::vault(uint256(1)));
|
||||
vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
|
||||
vault->at(sfAssetsTotal) = assetsTotal;
|
||||
if (!asset.integral())
|
||||
vault->at(sfScale) = scaleValue;
|
||||
if (version)
|
||||
vault->at(sfLEVersion) = std::to_underlying(*version);
|
||||
if (version == VaultVersion::FixedPrecision)
|
||||
{
|
||||
vault->at(sfAssetsAvailable) = assetsAvailable.value_or(assetsTotal - assetsDeployed);
|
||||
vault->at(sfAssetsDeployed) = assetsDeployed;
|
||||
}
|
||||
else if (assetsAvailable)
|
||||
{
|
||||
vault->at(sfAssetsAvailable) = *assetsAvailable;
|
||||
}
|
||||
associateAsset(*vault, asset);
|
||||
return vault;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -32,8 +32,6 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
auto const assetsAvailableValue = canonical_NUMBER();
|
||||
auto const assetsMaximumValue = canonical_NUMBER();
|
||||
auto const lossUnrealizedValue = canonical_NUMBER();
|
||||
auto const yieldUnrealizedValue = canonical_NUMBER();
|
||||
auto const assetsDeployedValue = canonical_NUMBER();
|
||||
auto const shareMPTIDValue = canonical_UINT192();
|
||||
auto const withdrawalPolicyValue = canonical_UINT8();
|
||||
auto const scaleValue = canonical_UINT8();
|
||||
@@ -59,8 +57,6 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
builder.setAssetsAvailable(assetsAvailableValue);
|
||||
builder.setAssetsMaximum(assetsMaximumValue);
|
||||
builder.setLossUnrealized(lossUnrealizedValue);
|
||||
builder.setYieldUnrealized(yieldUnrealizedValue);
|
||||
builder.setAssetsDeployed(assetsDeployedValue);
|
||||
builder.setScale(scaleValue);
|
||||
builder.setLEVersion(lEVersionValue);
|
||||
builder.setVaultKind(vaultKindValue);
|
||||
@@ -170,22 +166,6 @@ TEST(VaultTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasLossUnrealized());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = yieldUnrealizedValue;
|
||||
auto const actualOpt = entry.getYieldUnrealized();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfYieldUnrealized");
|
||||
EXPECT_TRUE(entry.hasYieldUnrealized());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = assetsDeployedValue;
|
||||
auto const actualOpt = entry.getAssetsDeployed();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfAssetsDeployed");
|
||||
EXPECT_TRUE(entry.hasAssetsDeployed());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = scaleValue;
|
||||
auto const actualOpt = entry.getScale();
|
||||
@@ -251,8 +231,6 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
auto const assetsAvailableValue = canonical_NUMBER();
|
||||
auto const assetsMaximumValue = canonical_NUMBER();
|
||||
auto const lossUnrealizedValue = canonical_NUMBER();
|
||||
auto const yieldUnrealizedValue = canonical_NUMBER();
|
||||
auto const assetsDeployedValue = canonical_NUMBER();
|
||||
auto const shareMPTIDValue = canonical_UINT192();
|
||||
auto const withdrawalPolicyValue = canonical_UINT8();
|
||||
auto const scaleValue = canonical_UINT8();
|
||||
@@ -275,8 +253,6 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfAssetsAvailable) = assetsAvailableValue;
|
||||
sle->at(sfAssetsMaximum) = assetsMaximumValue;
|
||||
sle->at(sfLossUnrealized) = lossUnrealizedValue;
|
||||
sle->at(sfYieldUnrealized) = yieldUnrealizedValue;
|
||||
sle->at(sfAssetsDeployed) = assetsDeployedValue;
|
||||
sle->at(sfShareMPTID) = shareMPTIDValue;
|
||||
sle->at(sfWithdrawalPolicy) = withdrawalPolicyValue;
|
||||
sle->at(sfScale) = scaleValue;
|
||||
@@ -449,32 +425,6 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfLossUnrealized");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = yieldUnrealizedValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getYieldUnrealized();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getYieldUnrealized();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfYieldUnrealized");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfYieldUnrealized");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = assetsDeployedValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getAssetsDeployed();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getAssetsDeployed();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfAssetsDeployed");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfAssetsDeployed");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = scaleValue;
|
||||
|
||||
@@ -620,10 +570,6 @@ TEST(VaultTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getAssetsMaximum().has_value());
|
||||
EXPECT_FALSE(entry.hasLossUnrealized());
|
||||
EXPECT_FALSE(entry.getLossUnrealized().has_value());
|
||||
EXPECT_FALSE(entry.hasYieldUnrealized());
|
||||
EXPECT_FALSE(entry.getYieldUnrealized().has_value());
|
||||
EXPECT_FALSE(entry.hasAssetsDeployed());
|
||||
EXPECT_FALSE(entry.getAssetsDeployed().has_value());
|
||||
EXPECT_FALSE(entry.hasScale());
|
||||
EXPECT_FALSE(entry.getScale().has_value());
|
||||
EXPECT_FALSE(entry.hasLEVersion());
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
#include <xrpld/rpc/detail/Handler.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
#include <xrpld/rpc/detail/PathRequestManager.h>
|
||||
#include <xrpld/rpc/detail/Pathfinder.h>
|
||||
#include <xrpld/shamap/NodeFamily.h>
|
||||
@@ -1460,20 +1461,25 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
//
|
||||
// Execute start up rpc commands.
|
||||
//
|
||||
for (auto const& cmd : config_->section(Sections::kRpcStartup).lines())
|
||||
auto const& startupCommands = config_->section(Sections::kRpcStartup).lines();
|
||||
for (std::size_t i = 0; i < startupCommands.size(); ++i)
|
||||
{
|
||||
json::Reader jrReader;
|
||||
json::Value jvCommand;
|
||||
|
||||
if (!jrReader.parse(cmd, jvCommand))
|
||||
// A line that does not parse has no field to mask, so it is reported by position and
|
||||
// parser message only, and no command runs for it.
|
||||
if (!jrReader.parse(startupCommands[i], jvCommand))
|
||||
{
|
||||
JLOG(journal_.fatal())
|
||||
<< "Couldn't parse entry in [" << Sections::kRpcStartup << "]: '" << cmd;
|
||||
<< "Couldn't parse entry " << (i + 1) << " in [" << Sections::kRpcStartup
|
||||
<< "]: " << jrReader.getFormattedErrorMessages();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!config_->quiet())
|
||||
{
|
||||
JLOG(journal_.fatal()) << "Startup RPC: " << jvCommand << std::endl;
|
||||
JLOG(journal_.fatal()) << "Startup RPC: " << rpc::loggable(jvCommand) << std::endl;
|
||||
}
|
||||
|
||||
resource::Charge loadType = resource::kFeeReferenceRpc;
|
||||
@@ -1499,7 +1505,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
|
||||
if (!config_->quiet())
|
||||
{
|
||||
JLOG(journal_.fatal()) << "Result: " << jvResult << std::endl;
|
||||
JLOG(journal_.fatal()) << "Result: " << rpc::loggable(jvResult) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3884,37 +3884,32 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
HashSet<InfoSub::pointer> notify;
|
||||
int iProposed = 0;
|
||||
|
||||
std::vector<SubAccountHistoryInfo> accountHistoryNotify;
|
||||
|
||||
{
|
||||
std::scoped_lock const sl(accountLock_);
|
||||
|
||||
if (subRTAccount_.empty())
|
||||
return;
|
||||
|
||||
if (!subAccount_.empty() || !subRTAccount_.empty() || !subAccountHistory_.empty())
|
||||
for (auto const& affectedAccount : tx->getMentionedAccounts())
|
||||
{
|
||||
for (auto const& affectedAccount : tx->getMentionedAccounts())
|
||||
if (auto const simiIt = subRTAccount_.find(affectedAccount);
|
||||
simiIt != subRTAccount_.end())
|
||||
{
|
||||
if (auto const simiIt = subRTAccount_.find(affectedAccount);
|
||||
simiIt != subRTAccount_.end())
|
||||
auto it = simiIt->second.begin();
|
||||
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
auto it = simiIt->second.begin();
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
|
||||
while (it != simiIt->second.end())
|
||||
if (p)
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
++it;
|
||||
++iProposed;
|
||||
}
|
||||
else
|
||||
{
|
||||
it = simiIt->second.erase(it);
|
||||
}
|
||||
notify.insert(p);
|
||||
++it;
|
||||
++iProposed;
|
||||
}
|
||||
else
|
||||
{
|
||||
it = simiIt->second.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3923,10 +3918,10 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
|
||||
JLOG(journal_.trace()) << "pubProposedAccountTransaction: " << iProposed;
|
||||
|
||||
if (!notify.empty() || !accountHistoryNotify.empty())
|
||||
if (!notify.empty())
|
||||
{
|
||||
// Create two different Json objects, for different API versions
|
||||
MultiApiJson jvObj = transJson(tx, result, false, ledger, std::nullopt);
|
||||
MultiApiJson const jvObj = transJson(tx, result, false, ledger, std::nullopt);
|
||||
|
||||
for (InfoSub::Ref isrListener : notify)
|
||||
{
|
||||
@@ -3934,21 +3929,6 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
isrListener->getApiVersion(), //
|
||||
[&](json::Value const& jv) { isrListener->send(jv, true); });
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
jvObj.isMember(jss::account_history_tx_stream) == MultiApiJson::IsMemberResult::None,
|
||||
"xrpl::NetworkOPs::pubProposedAccountTransaction : "
|
||||
"account_history_tx_stream not set");
|
||||
for (auto& info : accountHistoryNotify)
|
||||
{
|
||||
auto const& index = info.index;
|
||||
if (index->forwardTxIndex == 0 && !index->haveHistorical)
|
||||
jvObj.set(jss::account_history_tx_first, true);
|
||||
jvObj.set(jss::account_history_tx_index, index->forwardTxIndex++);
|
||||
jvObj.visit(
|
||||
info.sink->getApiVersion(), //
|
||||
[&](json::Value const& jv) { info.sink->send(jv, true); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,6 +171,18 @@ public:
|
||||
void
|
||||
onRequest(Session& session);
|
||||
|
||||
/**
|
||||
* Receives one WebSocket frame.
|
||||
*
|
||||
* A frame that exceeds the request size limit, does not parse or is not
|
||||
* an object is answered here with `jsonInvalid` and the frame's `size`;
|
||||
* its body is never echoed, since an unparsed body has no fields to mask.
|
||||
* Any other frame is posted to the job queue and answered from
|
||||
* processSession.
|
||||
*
|
||||
* @param session The WebSocket session the frame arrived on.
|
||||
* @param buffers The frame's bytes.
|
||||
*/
|
||||
void
|
||||
onWSMessage(
|
||||
std::shared_ptr<WSSession> session,
|
||||
@@ -183,15 +195,54 @@ public:
|
||||
onStopped(Server&);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Serves one parsed WebSocket request.
|
||||
*
|
||||
* Closes the connection when its resource balance is past the drop
|
||||
* threshold. Otherwise checks the API version and the `command` and
|
||||
* `method` fields, dispatches through rpc::doCommand, charges the
|
||||
* session's consumer, and shapes the reply as a `response`, echoing the
|
||||
* masked request on an error.
|
||||
*
|
||||
* @param session The session the request arrived on.
|
||||
* @param coro The coroutine the request runs on.
|
||||
* @param jv The parsed request.
|
||||
* @return The reply to send.
|
||||
*/
|
||||
json::Value
|
||||
processSession(
|
||||
std::shared_ptr<WSSession> const& session,
|
||||
std::shared_ptr<JobQueue::Coro> const& coro,
|
||||
json::Value const& jv);
|
||||
|
||||
/**
|
||||
* Serves one HTTP request on a coroutine: hands the body, the client
|
||||
* address and the forwarding headers to processRequest, then completes
|
||||
* or closes the session as its keep-alive header asks.
|
||||
*
|
||||
* @param session The HTTP session the request arrived on.
|
||||
* @param coro The coroutine the request runs on.
|
||||
*/
|
||||
void
|
||||
processSession(std::shared_ptr<Session> const&, std::shared_ptr<JobQueue::Coro> coro);
|
||||
processSession(std::shared_ptr<Session> const& session, std::shared_ptr<JobQueue::Coro> coro);
|
||||
|
||||
/**
|
||||
* Serves one HTTP body: parses it, answers a malformed or unauthorized
|
||||
* request with a plain-text status, serves a `method: "batch"` body entry
|
||||
* by entry, dispatches each request through rpc::doCommand, and writes
|
||||
* the reply with its HTTP status. An error reply echoes the masked
|
||||
* request.
|
||||
*
|
||||
* @param port The port the request arrived on, for its role and limits.
|
||||
* @param request The raw body.
|
||||
* @param remoteIPAddress The client address, for resource accounting and
|
||||
* the role.
|
||||
* @param output Where the reply bytes are written.
|
||||
* @param coro The coroutine the request runs on.
|
||||
* @param forwardedFor The `X-Forwarded-For` header, when the port trusts
|
||||
* a proxy.
|
||||
* @param user The `X-User` header.
|
||||
*/
|
||||
void
|
||||
processRequest(
|
||||
Port const& port,
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Reports whether @p name is one of kCredentialFields.
|
||||
*
|
||||
* An array element's memberName is empty, which no credential is spelled as,
|
||||
* so an element is never a credential itself and is descended into.
|
||||
*
|
||||
* @param name The member name to look up.
|
||||
* @return True if @p name is a credential-bearing field.
|
||||
*/
|
||||
bool
|
||||
isCredential(std::string_view name)
|
||||
{
|
||||
return !name.empty() && std::ranges::find(kCredentialFields, name) != kCredentialFields.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces every credential-bearing field of @p value in place, at every depth.
|
||||
*
|
||||
* @param value The object or array to mask. Any other value is left alone.
|
||||
*/
|
||||
void
|
||||
maskInPlace(json::Value& value)
|
||||
{
|
||||
if (!value.isObject() && !value.isArray())
|
||||
return;
|
||||
|
||||
for (auto it = value.begin(); it != value.end(); ++it)
|
||||
{
|
||||
// A credential is replaced whatever it holds, so it is not descended into.
|
||||
if (isCredential(it.memberName()))
|
||||
{
|
||||
*it = json::Value{"<masked>"};
|
||||
}
|
||||
else
|
||||
{
|
||||
maskInPlace(*it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
hasSecret(json::Value const& value)
|
||||
{
|
||||
if (!value.isObject() && !value.isArray())
|
||||
return false;
|
||||
|
||||
for (auto it = value.begin(); it != value.end(); ++it)
|
||||
{
|
||||
if (isCredential(it.memberName()) || hasSecret(*it))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
json::Value
|
||||
maskSecrets(json::Value const& request)
|
||||
{
|
||||
auto masked = request;
|
||||
maskInPlace(masked);
|
||||
return masked;
|
||||
}
|
||||
|
||||
std::string
|
||||
loggable(json::Value const& value)
|
||||
{
|
||||
// The copy the mask takes is paid only when there is a credential to replace.
|
||||
auto text = hasSecret(value) ? to_string(maskSecrets(value)) : to_string(value);
|
||||
if (text.size() > kMaxLoggedChars)
|
||||
text.resize(kMaxLoggedChars);
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
/**
|
||||
* Fields that carry a credential, in a request or in a reply.
|
||||
*
|
||||
* Literals rather than jss entries, since not every one has a jss entry.
|
||||
*
|
||||
* `username` and `password` are `subscribe`'s deprecated spellings of
|
||||
* `url_username` and `url_password`, which is why a name identifying rather
|
||||
* than authenticating is on the list. The `master_` and `validation_` names
|
||||
* come from `wallet_propose` and `validation_create` replies, and a reply is
|
||||
* logged like a request. `master_key` is also the public key that
|
||||
* `validator_info` and the `manifests` and `validations` streams report, and
|
||||
* it is masked with the rest.
|
||||
*/
|
||||
inline constexpr std::array<std::string_view, 16> kCredentialFields{
|
||||
"admin_password",
|
||||
"admin_user",
|
||||
"master_key",
|
||||
"master_seed",
|
||||
"master_seed_hex",
|
||||
"passphrase",
|
||||
"password",
|
||||
"secret",
|
||||
"seed",
|
||||
"seed_hex",
|
||||
"url_password",
|
||||
"url_username",
|
||||
"username",
|
||||
"validation_key",
|
||||
"validation_private_key",
|
||||
"validation_seed",
|
||||
};
|
||||
|
||||
/**
|
||||
* How much of a request or a reply a log line carries.
|
||||
*
|
||||
* A client chooses the size of what it sends, so nothing it cannot choose
|
||||
* bounds the length.
|
||||
*/
|
||||
inline constexpr std::size_t kMaxLoggedChars = 10000;
|
||||
|
||||
/**
|
||||
* Returns a copy of @p request with every credential-bearing field replaced by
|
||||
* a placeholder.
|
||||
*
|
||||
* Apply to every request echoed back to a caller. Masks at every depth, since
|
||||
* the JSON-RPC transport nests a credential inside `params` and a batch nests
|
||||
* one request per entry. Recursion is bounded by json::Reader::kNestLimit.
|
||||
*
|
||||
* @param request The request to mask.
|
||||
* @return The masked copy.
|
||||
*/
|
||||
[[nodiscard]] json::Value
|
||||
maskSecrets(json::Value const& request);
|
||||
|
||||
/**
|
||||
* Reports whether @p value carries a credential-bearing field at any depth.
|
||||
*
|
||||
* Walks objects and arrays without copying and stops at the first field named
|
||||
* in kCredentialFields, so a caller pays for masking only when there is
|
||||
* something to mask. A value with no members carries none.
|
||||
*
|
||||
* @param value The request or reply to inspect.
|
||||
* @return True if a credential-bearing field is present at any depth.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
hasSecret(json::Value const& value);
|
||||
|
||||
/**
|
||||
* Renders @p value for a log line: masked when it carries a credential, then
|
||||
* truncated to kMaxLoggedChars.
|
||||
*
|
||||
* Every site that writes a request or a reply body to the log goes through
|
||||
* this. A value with no credential is serialized as it is, so the copy the
|
||||
* mask takes is paid only when hasSecret() reports one.
|
||||
*
|
||||
* Call it inside the JLOG argument and never before the macro: that argument is
|
||||
* evaluated only when the sink is active, so hoisting the call puts the walk
|
||||
* and the serialization on every request.
|
||||
*
|
||||
* Truncation is by bytes, so a multi-byte character can be split.
|
||||
*
|
||||
* @param value The request or reply to render.
|
||||
* @return The masked, truncated text.
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
loggable(json::Value const& value);
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/rpc/MethodNames.h>
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -23,6 +24,7 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
@@ -614,21 +616,32 @@ private:
|
||||
return rpcError(RpcInvalidParams);
|
||||
}
|
||||
|
||||
// json <command> <json>
|
||||
/**
|
||||
* Parses the `json <command> <json>` form: the second argument is parsed
|
||||
* as the request and the first becomes its `method`. The request is
|
||||
* logged masked once parsed; the unparsed text never is.
|
||||
*
|
||||
* @param jvParams The command name and the JSON text.
|
||||
* @return The request, or `invalidParams` when the text does not parse
|
||||
* to an object.
|
||||
*/
|
||||
json::Value
|
||||
parseJson(json::Value const& jvParams)
|
||||
{
|
||||
json::Reader reader;
|
||||
json::Value jvRequest;
|
||||
|
||||
// The command name cannot carry a credential, so it logs whether the JSON parses or not.
|
||||
JLOG(j_.trace()) << "RPC method: " << jvParams[0u];
|
||||
JLOG(j_.trace()) << "RPC json: " << jvParams[1u];
|
||||
|
||||
if (reader.parse(jvParams[1u].asString(), jvRequest))
|
||||
{
|
||||
if (!jvRequest.isObjectOrNull())
|
||||
return rpcError(RpcInvalidParams);
|
||||
|
||||
// Logged only once parsed, so a signing secret among the members can be masked.
|
||||
JLOG(j_.trace()) << "RPC json: " << rpc::loggable(jvRequest);
|
||||
|
||||
jvRequest[jss::method] = jvParams[0u];
|
||||
|
||||
return jvRequest;
|
||||
@@ -637,6 +650,16 @@ private:
|
||||
return rpcError(RpcInvalidParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether @p jv is a request this client accepts as JSON-RPC 2.0:
|
||||
* an object whose `jsonrpc` is rpc::kJsonRpcVersion and `ripplerpc` is
|
||||
* rpc::kRippleRpcVersion2, carrying an `id` and a `method`, with `params`
|
||||
* absent, null, an array or an object; or a non-empty array of such
|
||||
* objects.
|
||||
*
|
||||
* @param jv The parsed argument.
|
||||
* @return True when the shape is accepted.
|
||||
*/
|
||||
bool
|
||||
isValidJson2(json::Value const& jv)
|
||||
{
|
||||
@@ -651,8 +674,8 @@ private:
|
||||
}
|
||||
if (jv.isObject())
|
||||
{
|
||||
if (jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0" &&
|
||||
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0" &&
|
||||
if (jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == rpc::kJsonRpcVersion &&
|
||||
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == rpc::kRippleRpcVersion2 &&
|
||||
jv.isMember(jss::id) && jv.isMember(jss::method))
|
||||
{
|
||||
return !jv.isMember(jss::params) ||
|
||||
@@ -1010,7 +1033,14 @@ private:
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// ripple_path_find <json> [<ledger>]
|
||||
/**
|
||||
* Parses the `ripple_path_find <json> [<ledger>]` form: the first
|
||||
* argument is parsed as the request and logged masked once parsed, the
|
||||
* optional second names the ledger.
|
||||
*
|
||||
* @param jvParams The JSON text and, optionally, the ledger.
|
||||
* @return The request, or `invalidParams` when the text does not parse.
|
||||
*/
|
||||
json::Value
|
||||
parseRipplePathFind(json::Value const& jvParams)
|
||||
{
|
||||
@@ -1018,10 +1048,12 @@ private:
|
||||
json::Value jvRequest{json::ValueType::Object};
|
||||
bool const bLedger = 2 == jvParams.size();
|
||||
|
||||
JLOG(j_.trace()) << "RPC json: " << jvParams[0u];
|
||||
|
||||
if (reader.parse(jvParams[0u].asString(), jvRequest))
|
||||
{
|
||||
// The JSON is logged only once parsed, so a signing secret among the named members can
|
||||
// be masked. Unparsed it is a bare string with no members to name.
|
||||
JLOG(j_.trace()) << "RPC json: " << rpc::loggable(jvRequest);
|
||||
|
||||
if (bLedger)
|
||||
{
|
||||
jvParseLedger(jvRequest, jvParams[1u].asString());
|
||||
@@ -1750,11 +1782,7 @@ public:
|
||||
json::Value
|
||||
parseCommand(std::string_view strMethod, json::Value const& jvParams, bool allowAnyCommand)
|
||||
{
|
||||
if (auto const stream = j_.trace())
|
||||
{
|
||||
stream << "Method: '" << strMethod << "'";
|
||||
stream << "Params: " << jvParams;
|
||||
}
|
||||
JLOG(j_.trace()) << "Method: '" << strMethod << "'";
|
||||
|
||||
auto const found = std::ranges::lower_bound(kSortedCommands, strMethod, {}, &Command::name);
|
||||
|
||||
@@ -1861,15 +1889,26 @@ struct RPCCallImp
|
||||
"process.");
|
||||
}
|
||||
|
||||
// Parse reply
|
||||
JLOG(j.debug()) << "RPC reply: " << strData << std::endl;
|
||||
// Parse reply. A plain text body is a rejection, which carries no field a credential
|
||||
// could sit in, so it is logged as it is, capped at kMaxLoggedChars.
|
||||
if (strData.starts_with("Unable to parse request") ||
|
||||
strData.starts_with(jss::invalid_API_version.cStr()))
|
||||
{
|
||||
JLOG(j.debug()) << "RPC reply: "
|
||||
<< std::string_view{strData}.substr(0, rpc::kMaxLoggedChars);
|
||||
Throw<RequestNotParsable>(strData);
|
||||
}
|
||||
json::Reader reader;
|
||||
json::Value jvReply;
|
||||
if (!reader.parse(strData, jvReply))
|
||||
{
|
||||
JLOG(j.debug()) << "RPC reply: "
|
||||
<< std::string_view{strData}.substr(0, rpc::kMaxLoggedChars);
|
||||
Throw<std::runtime_error>("couldn't parse reply from server");
|
||||
}
|
||||
// Logged once parsed, masked: `validation_create` and `wallet_propose` answer with a
|
||||
// private key, which the fallback scrubber in Log.cpp does not know by every name.
|
||||
JLOG(j.debug()) << "RPC reply: " << rpc::loggable(jvReply);
|
||||
|
||||
if (!jvReply)
|
||||
Throw<std::runtime_error>("expected reply to have result, error and id properties");
|
||||
@@ -1911,7 +1950,16 @@ commandLineMethodNames()
|
||||
return RPCParser::methodNames();
|
||||
}
|
||||
|
||||
// Used internally by rpcClient.
|
||||
/**
|
||||
* Translates command line arguments into the request the client sends.
|
||||
*
|
||||
* @param args The method name followed by its positional arguments.
|
||||
* @param retParams Receives the arguments as `method` and `params`, the form
|
||||
* `rpc` echoes on an error.
|
||||
* @param apiVersion The `api_version` stamped on each request naming none.
|
||||
* @param j The journal the built request is logged to, masked.
|
||||
* @return The request, an array of requests, or the parser's error object.
|
||||
*/
|
||||
json::Value
|
||||
rpcCmdToJson(
|
||||
std::vector<std::string> const& args,
|
||||
@@ -1951,12 +1999,27 @@ rpcCmdToJson(
|
||||
std::for_each(jvRequest.begin(), jvRequest.end(), insertApiVersion);
|
||||
}
|
||||
|
||||
JLOG(j.trace()) << "RPC Request: " << jvRequest << std::endl;
|
||||
// `sign` and `submit` put the signing secret in the request the command line built.
|
||||
JLOG(j.trace()) << "RPC Request: " << rpc::loggable(jvRequest) << std::endl;
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Runs one command line command against the configured server.
|
||||
*
|
||||
* Builds the request, adds the admin credentials from the config, sends it
|
||||
* over HTTP and unwraps the result. On an error the output also carries the
|
||||
* invocation as `rpc` and the request as `request_sent`, masked.
|
||||
*
|
||||
* @param args The method name followed by its arguments.
|
||||
* @param config The client configuration naming the server and credentials.
|
||||
* @param logs The log manager.
|
||||
* @param apiVersion The `api_version` to request.
|
||||
* @param headers Extra HTTP headers to send.
|
||||
* @return The exit code and the output to print.
|
||||
*/
|
||||
std::pair<int, json::Value>
|
||||
rpcClient(
|
||||
std::vector<std::string> const& args,
|
||||
@@ -2068,7 +2131,9 @@ rpcClient(
|
||||
if (jvOutput.isMember(jss::error))
|
||||
{
|
||||
jvOutput["rpc"] = jvRpc; // How the command was seen as method + params.
|
||||
jvOutput["request_sent"] = jvRequest; // How the command was translated.
|
||||
// Carries the config's `admin_password` and prints to stdout, so it is masked.
|
||||
// `jvRpc` above is positional, so masking by member name does not reach it.
|
||||
jvOutput["request_sent"] = rpc::maskSecrets(jvRequest);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/Handler.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -136,7 +137,7 @@ fillHandler(JsonContext& context, Handler const*& result)
|
||||
: context.params[jss::method].asString();
|
||||
|
||||
JLOG(context.j.trace()) << "COMMAND:" << strCommand;
|
||||
JLOG(context.j.trace()) << "REQUEST:" << context.params;
|
||||
JLOG(context.j.trace()) << "REQUEST:" << loggable(context.params);
|
||||
auto handler = getHandler(context.apiVersion, context.app.config().betaRpcApi, strCommand);
|
||||
|
||||
if (handler == nullptr)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpld/rpc/RPCSub.h>
|
||||
|
||||
#include <xrpld/rpc/RPCCall.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
@@ -87,7 +88,7 @@ public:
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
auto const jm = broadcast ? j_.debug() : j_.info();
|
||||
JLOG(jm) << "rpc_call::fromNetwork push: " << jvObj;
|
||||
JLOG(jm) << "rpc_call::fromNetwork push: " << rpc::loggable(jvObj);
|
||||
|
||||
deque_.emplace_back(seq_++, jvObj);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/rpc/RPCHandler.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
#include <xrpld/rpc/detail/WSInfoSub.h>
|
||||
|
||||
@@ -60,7 +61,9 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -340,10 +343,13 @@ ServerHandler::onWSMessage(
|
||||
auto const size = boost::asio::buffer_size(buffers);
|
||||
if (size > rpc::tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject())
|
||||
{
|
||||
// An unparsed body cannot be masked field-wise, so its size goes instead of its content.
|
||||
// Clamped rather than narrowed: json has no integer wider than 32 bits.
|
||||
json::Value jvResult(json::ValueType::Object);
|
||||
jvResult[jss::type] = jss::error;
|
||||
jvResult[jss::error] = "jsonInvalid";
|
||||
jvResult[jss::value] = buffersToString(buffers);
|
||||
jvResult[jss::size] =
|
||||
json::UInt(std::min<std::size_t>(size, std::numeric_limits<json::UInt>::max()));
|
||||
boost::beast::multi_buffer sb;
|
||||
json::stream(jvResult, [&sb](auto const p, auto const n) {
|
||||
sb.commit(boost::asio::buffer_copy(sb.prepare(n), boost::asio::buffer(p, n)));
|
||||
@@ -354,7 +360,7 @@ ServerHandler::onWSMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
JLOG(journal_.trace()) << "Websocket received '" << jv << "'";
|
||||
JLOG(journal_.trace()) << "Websocket received '" << rpc::loggable(jv) << "'";
|
||||
|
||||
auto const postResult = jobQueue_.postCoro(
|
||||
JtClientWebsocket,
|
||||
@@ -392,22 +398,35 @@ ServerHandler::onStopped(Server&)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Logs how long a request took.
|
||||
*
|
||||
* A slow request is reported at warn from one second and at error from ten,
|
||||
* with the duration only. The request is logged at debug, since it is client
|
||||
* text.
|
||||
*
|
||||
* @param request The request the duration belongs to.
|
||||
* @param duration How long processing it took.
|
||||
* @param journal Where the line is written.
|
||||
*/
|
||||
template <class T>
|
||||
void
|
||||
logDuration(json::Value const& request, T const& duration, beast::Journal& journal)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
auto const level = [&] {
|
||||
if (duration >= 10s)
|
||||
return journal.error();
|
||||
if (duration >= 1s)
|
||||
return journal.warn();
|
||||
return journal.debug();
|
||||
}();
|
||||
auto const micros = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
|
||||
|
||||
JLOG(level) << "RPC request processing duration = "
|
||||
<< std::chrono::duration_cast<std::chrono::microseconds>(duration).count()
|
||||
<< " microseconds. request = " << request;
|
||||
if (duration >= 1s)
|
||||
{
|
||||
auto const slow = duration >= 10s ? journal.error() : journal.warn();
|
||||
JLOG(slow) << "RPC request processing duration = " << micros << " microseconds.";
|
||||
}
|
||||
|
||||
if (auto const stream = journal.debug())
|
||||
{
|
||||
stream << "RPC request processing duration = " << micros
|
||||
<< " microseconds. request = " << rpc::loggable(request);
|
||||
}
|
||||
}
|
||||
|
||||
json::Value
|
||||
@@ -442,7 +461,7 @@ ServerHandler::processSession(
|
||||
jr[jss::status] = jss::error;
|
||||
jr[jss::error] = apiVersion == rpc::kApiInvalidVersion ? jss::invalid_API_version
|
||||
: jss::missingCommand;
|
||||
jr[jss::request] = jv;
|
||||
jr[jss::request] = rpc::maskSecrets(jv);
|
||||
if (jv.isMember(jss::id))
|
||||
jr[jss::id] = jv[jss::id];
|
||||
if (jv.isMember(jss::jsonrpc))
|
||||
@@ -498,11 +517,9 @@ ServerHandler::processSession(
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
jr[jss::result] = rpc::makeError(RpcInternal);
|
||||
JLOG(journal_.error()) << "Exception while processing WS: " << ex.what() << "\n"
|
||||
<< "Input JSON: " << json::Compact{json::Value{jv}};
|
||||
// LCOV_EXCL_STOP
|
||||
<< "Input JSON: " << rpc::loggable(jv);
|
||||
}
|
||||
|
||||
is->getConsumer().charge(loadType);
|
||||
@@ -519,21 +536,7 @@ ServerHandler::processSession(
|
||||
jr = jr[jss::result];
|
||||
jr[jss::status] = jss::error;
|
||||
|
||||
auto rq = jv;
|
||||
|
||||
if (rq.isObject())
|
||||
{
|
||||
if (rq.isMember(jss::passphrase.cStr()))
|
||||
rq[jss::passphrase.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::secret.cStr()))
|
||||
rq[jss::secret.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::seed.cStr()))
|
||||
rq[jss::seed.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::seed_hex.cStr()))
|
||||
rq[jss::seed_hex.cStr()] = "<masked>";
|
||||
}
|
||||
|
||||
jr[jss::request] = rq;
|
||||
jr[jss::request] = rpc::maskSecrets(jv);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -599,6 +602,31 @@ constexpr json::Int kServerOverloaded = -32604;
|
||||
constexpr json::Int kForbidden = -32605;
|
||||
constexpr json::Int kWrongVersion = -32606;
|
||||
|
||||
/**
|
||||
* The HTTP status the `ripplerpc: "3.0"` envelope reports for @p code.
|
||||
*
|
||||
* The codes below answer 200, which is what a client calling `account_info`
|
||||
* this way already reads. The list is closed: a code belongs on it only if it
|
||||
* already answered a coded reply at this status.
|
||||
*
|
||||
* @param code The error code the reply reports.
|
||||
* @return The HTTP status the reply is sent with.
|
||||
*/
|
||||
static int
|
||||
legacyHttpStatus(ErrorCodeI code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case RpcActMalformed:
|
||||
case RpcActNotFound:
|
||||
case RpcAlreadyMultisig:
|
||||
case RpcAlreadySingleSig:
|
||||
return 200;
|
||||
default:
|
||||
return rpc::errorCodeHttpStatus(code);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::processRequest(
|
||||
Port const& port,
|
||||
@@ -628,7 +656,9 @@ ServerHandler::processRequest(
|
||||
|
||||
bool batch = false;
|
||||
unsigned size = 1;
|
||||
if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == "batch")
|
||||
// Spelled as a view so the name is compared in place. A bare literal would reach the
|
||||
// Value-to-Value comparison instead, which builds a Value from it, allocating per request.
|
||||
if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == std::string_view{"batch"})
|
||||
{
|
||||
batch = true;
|
||||
if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray())
|
||||
@@ -648,7 +678,7 @@ ServerHandler::processRequest(
|
||||
if (!jsonRPC.isObject())
|
||||
{
|
||||
json::Value r(json::ValueType::Object);
|
||||
r[jss::request] = jsonRPC;
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "Method not found");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -676,7 +706,7 @@ ServerHandler::processRequest(
|
||||
return;
|
||||
}
|
||||
json::Value r(json::ValueType::Object);
|
||||
r[jss::request] = jsonRPC;
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kWrongVersion, jss::invalid_API_version.cStr());
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -718,7 +748,7 @@ ServerHandler::processRequest(
|
||||
httpReply(503, "Server is overloaded", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kServerOverloaded, "Server is overloaded");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -733,7 +763,7 @@ ServerHandler::processRequest(
|
||||
httpReply(403, "Forbidden", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kForbidden, "Forbidden");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -747,7 +777,7 @@ ServerHandler::processRequest(
|
||||
httpReply(400, "Null method", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "Null method");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -762,7 +792,7 @@ ServerHandler::processRequest(
|
||||
httpReply(400, "method is not string", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "method is not string");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -777,7 +807,7 @@ ServerHandler::processRequest(
|
||||
httpReply(400, "method is empty", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "method is empty");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -819,7 +849,7 @@ ServerHandler::processRequest(
|
||||
params = jsonRPC;
|
||||
}
|
||||
|
||||
std::string ripplerpc = "1.0";
|
||||
std::string ripplerpc{rpc::kRippleRpcVersion1};
|
||||
if (params.isMember(jss::ripplerpc))
|
||||
{
|
||||
if (!params[jss::ripplerpc].isString())
|
||||
@@ -831,7 +861,7 @@ ServerHandler::processRequest(
|
||||
return;
|
||||
}
|
||||
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "ripplerpc is not a string");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -849,11 +879,11 @@ ServerHandler::processRequest(
|
||||
user.remove_suffix(user.size());
|
||||
}
|
||||
|
||||
JLOG(journal_.debug()) << "Query: " << strMethod << params;
|
||||
JLOG(journal_.debug()) << "Query: " << strMethod << rpc::loggable(params);
|
||||
|
||||
// Provide the JSON-RPC method as the field "command" in the request.
|
||||
params[jss::command] = strMethod;
|
||||
JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << params;
|
||||
JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << rpc::loggable(params);
|
||||
|
||||
resource::Charge loadType = resource::kFeeReferenceRpc;
|
||||
|
||||
@@ -883,12 +913,9 @@ ServerHandler::processRequest(
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
result = rpc::makeError(RpcInternal);
|
||||
JLOG(journal_.error())
|
||||
<< "Internal error : " << ex.what()
|
||||
<< " when processing request: " << json::Compact{json::Value{params}};
|
||||
// LCOV_EXCL_STOP
|
||||
JLOG(journal_.error()) << "Internal error : " << ex.what()
|
||||
<< " when processing request: " << rpc::loggable(params);
|
||||
}
|
||||
|
||||
auto const end = std::chrono::system_clock::now();
|
||||
@@ -900,7 +927,7 @@ ServerHandler::processRequest(
|
||||
result[jss::warning] = jss::load;
|
||||
|
||||
json::Value r(json::ValueType::Object);
|
||||
if (ripplerpc >= "2.0")
|
||||
if (ripplerpc >= rpc::kRippleRpcVersion2)
|
||||
{
|
||||
if (result.isMember(jss::error))
|
||||
{
|
||||
@@ -924,22 +951,8 @@ ServerHandler::processRequest(
|
||||
// received.
|
||||
if (result.isMember(jss::error))
|
||||
{
|
||||
auto rq = params;
|
||||
|
||||
if (rq.isObject())
|
||||
{ // But mask potentially sensitive information.
|
||||
if (rq.isMember(jss::passphrase.cStr()))
|
||||
rq[jss::passphrase.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::secret.cStr()))
|
||||
rq[jss::secret.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::seed.cStr()))
|
||||
rq[jss::seed.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::seed_hex.cStr()))
|
||||
rq[jss::seed_hex.cStr()] = "<masked>";
|
||||
}
|
||||
|
||||
result[jss::status] = jss::error;
|
||||
result[jss::request] = rq;
|
||||
result[jss::request] = rpc::maskSecrets(params);
|
||||
|
||||
JLOG(journal_.debug())
|
||||
<< "rpcError: " << result[jss::error] << ": " << result[jss::error_message];
|
||||
@@ -982,14 +995,14 @@ ServerHandler::processRequest(
|
||||
// This feature is enabled with ripplerpc version 3.0 and above.
|
||||
// Before ripplerpc version 3.0 always return 200.
|
||||
if (reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc].isString() &&
|
||||
reply[jss::ripplerpc].asString() >= "3.0")
|
||||
reply[jss::ripplerpc].asString() >= rpc::kRippleRpcVersion3)
|
||||
{
|
||||
// If there's an error_code, use that to determine the HTTP Status.
|
||||
if (reply.isMember(jss::error) && reply[jss::error].isMember(jss::error_code) &&
|
||||
reply[jss::error][jss::error_code].isInt())
|
||||
{
|
||||
int const errCode = reply[jss::error][jss::error_code].asInt();
|
||||
return rpc::errorCodeHttpStatus(static_cast<ErrorCodeI>(errCode));
|
||||
return legacyHttpStatus(static_cast<ErrorCodeI>(errCode));
|
||||
}
|
||||
}
|
||||
// Return OK.
|
||||
@@ -1004,21 +1017,22 @@ ServerHandler::processRequest(
|
||||
++rpcRequests_;
|
||||
rpcSize_.notify(beast::insight::Event::value_type{response.size()});
|
||||
|
||||
response += '\n';
|
||||
|
||||
// The serialized reply is in hand, so it is logged as built; the masked copy and its second
|
||||
// serialization are paid for only when a credential has to be replaced.
|
||||
if (auto const stream = journal_.debug())
|
||||
{
|
||||
static int const kMaxSize = 10000;
|
||||
if (response.size() <= kMaxSize)
|
||||
if (rpc::hasSecret(reply))
|
||||
{
|
||||
stream << "Reply: " << response;
|
||||
stream << "Reply: " << rpc::loggable(reply);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream << "Reply: " << response.substr(0, kMaxSize);
|
||||
stream << "Reply: " << std::string_view{response}.substr(0, rpc::kMaxLoggedChars);
|
||||
}
|
||||
}
|
||||
|
||||
response += '\n';
|
||||
|
||||
httpReply(httpStatus, response, output, rpcJ);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/AssetCache.h>
|
||||
#include <xrpld/rpc/detail/LegacyPathFind.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
#include <xrpld/rpc/detail/Pathfinder.h>
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
@@ -1018,7 +1019,8 @@ transactionSign(
|
||||
// could change the signing prefix of an alternate signature field.
|
||||
std::shared_ptr<ReadView const> const ledger = app.getOpenLedger().current();
|
||||
auto const j = app.getJournal("RPCHandler");
|
||||
JLOG(j.debug()) << "transactionSign: " << jvRequest;
|
||||
// The next statement reads the signing secret out of this same request.
|
||||
JLOG(j.debug()) << "transactionSign: " << loggable(jvRequest);
|
||||
|
||||
// Add and amend fields based on the transaction type.
|
||||
SigningForParams signForParams;
|
||||
@@ -1055,7 +1057,7 @@ transactionSubmit(
|
||||
|
||||
auto const& ledger = app.getOpenLedger().current();
|
||||
auto const j = app.getJournal("RPCHandler");
|
||||
JLOG(j.debug()) << "transactionSubmit: " << jvRequest;
|
||||
JLOG(j.debug()) << "transactionSubmit: " << loggable(jvRequest);
|
||||
|
||||
// Add and amend fields based on the transaction type.
|
||||
SigningForParams signForParams;
|
||||
@@ -1177,7 +1179,7 @@ transactionSignFor(
|
||||
{
|
||||
auto const& ledger = app.getOpenLedger().current();
|
||||
auto const j = app.getJournal("RPCHandler");
|
||||
JLOG(j.debug()) << "transactionSignFor: " << jvRequest;
|
||||
JLOG(j.debug()) << "transactionSignFor: " << loggable(jvRequest);
|
||||
|
||||
// Verify presence of the signer's account field.
|
||||
char const accountField[] = "account";
|
||||
@@ -1301,7 +1303,7 @@ transactionSubmitMultiSigned(
|
||||
{
|
||||
auto const& ledger = app.getOpenLedger().current();
|
||||
auto const j = app.getJournal("RPCHandler");
|
||||
JLOG(j.debug()) << "transactionSubmitMultiSigned: " << jvRequest;
|
||||
JLOG(j.debug()) << "transactionSubmitMultiSigned: " << loggable(jvRequest);
|
||||
|
||||
// When multi-signing, the "Sequence" and "SigningPubKey" fields must
|
||||
// be passed in by the caller.
|
||||
|
||||
Reference in new issue
Block a user