Compare commits

..

1 Commits

Author SHA1 Message Date
Timur Ialymov
e6a5157891 fix: Charge transfer fee on third-party vault withdrawals
A VaultWithdraw that names a Destination other than the submitter
delivered the asset with the issuer's transfer fee waived. Depositing
into a vault and withdrawing to someone else therefore moved IOUs and
MPTs without paying the fee.

Under fixCleanup3_5_0 the withdrawer's vault position pays the fee. A
fixed-asset request still delivers the requested amount and redeems
enough shares to cover the gross cost. A fixed-share request burns the
requested shares and delivers what is left after the fee. Withdrawals
to the submitter or to the issuer stay fee-free. The fee is settled by
redeeming the gross amount with the issuer and issuing the net amount
to the destination. ValidVault expects the destination delta to be the
vault outflow less the fee.
2026-09-22 22:17:02 +01:00
57 changed files with 696 additions and 561 deletions

View File

@@ -53,11 +53,6 @@ repos:
entry: ./bin/pre-commit/check_doxygen_style.py
language: python
types_or: [c++, c]
- id: fix-gtest-names
name: "fix gtest names: CamelCase suite, snake_case test case"
entry: ./bin/pre-commit/fix_gtest_names.py
language: python
types_or: [c++, c]
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: f4d7745e17a28aad7eed2f4874ca8d1568c11c4c # frozen: v22.1.8

View File

@@ -1,144 +0,0 @@
#!/usr/bin/env python3
"""
Rewrites gtest names to the required style in this project: the suite name is
CamelCase, the test-case name is snake_case.
TEST(SuiteName, test_case_name)
The gtest `DISABLED_` prefix is kept verbatim on either name.
Both conversions fold acronyms the way a reader expects:
`SetAndResetAccountTxnID` -> `set_and_reset_account_txn_id`, not
`set_and_reset_account_txn_i_d`.
The first argument of `TEST_F`, `TEST_P`, `TYPED_TEST` and `TYPED_TEST_P` is a
fixture class rather than a free identifier, so rewriting it here would leave
the class it names behind. Those are reported for a human to rename (clang-tidy
checks the class declaration itself, via readability-identifier-naming).
Usage: ./bin/pre-commit/fix_gtest_names.py <file1> <file2> ...
"""
import re
import sys
from collections import Counter
from pathlib import Path
# A test-case definition, `MACRO(SuiteOrFixture, TestName)`, anchored at the
# start of a line so that commented-out definitions and project macros that
# merely look similar (`TEST_EXPECT(...)`) are left alone. The `\s*` between
# arguments allows for a definition clang-format wrapped over several lines.
PATTERN = re.compile(
r"(?P<head>^[ \t]*(?P<macro>TYPED_TEST_P|TYPED_TEST|TEST_F|TEST_P|TEST)\s*\(\s*)"
r"(?P<suite>\w+)(?P<mid>\s*,\s*)(?P<name>\w+)(?P<tail>\s*\))",
re.MULTILINE,
)
# The macros whose first argument names a fixture class, not a free identifier.
FIXTURE_MACROS = ("TEST_F", "TEST_P", "TYPED_TEST", "TYPED_TEST_P")
DISABLED = "DISABLED_"
ACRONYM_BOUNDARY = re.compile(r"([A-Z]+)([A-Z][a-z])")
WORD_BOUNDARY = re.compile(r"([a-z\d])([A-Z])")
def _split_disabled(name: str) -> tuple[str, str]:
"""Splits off gtest's `DISABLED_` prefix, which is kept verbatim."""
if name.startswith(DISABLED):
return DISABLED, name[len(DISABLED) :]
return "", name
def snake_case(name: str) -> str:
"""Returns the name in snake_case, leaving acronyms whole.
`SetAndResetAccountTxnID` -> `set_and_reset_account_txn_id`,
`parseStatRSSkB` -> `parse_stat_rs_sk_b`.
"""
prefix, core = _split_disabled(name)
core = ACRONYM_BOUNDARY.sub(r"\1_\2", core)
return prefix + WORD_BOUNDARY.sub(r"\1_\2", core).lower()
def camel_case(name: str) -> str:
"""Returns the name in CamelCase, capitalizing each underscored word.
Only the letters that have to change are touched, so acronyms survive: a
conversion that went via snake_case would turn `SHAMapTest` into
`ShaMapTest`, whereas here it is already CamelCase and stays put.
`json_value` -> `JsonValue`, `parseStatRSSkB` -> `ParseStatRSSkB`.
"""
prefix, core = _split_disabled(name)
return prefix + "".join(w[:1].upper() + w[1:] for w in core.split("_") if w)
def _corrected(match: re.Match) -> tuple[str, str]:
"""Returns the suite and test-case names this definition should end up with."""
suite = match["suite"]
return (
suite if match["macro"] in FIXTURE_MACROS else camel_case(suite),
snake_case(match["name"]),
)
def fix_source(text: str) -> tuple[str, list[str]]:
"""Returns the corrected text and one `line: message` report per bad name."""
# gtest joins the suite and test names into one class name, so two test
# cases whose joined names agree cannot coexist: `TEST(a, b_c)` and
# `TEST(a_b, c)` both define `a_b_c_Test`. A rename that would introduce
# such a clash is reported for a human instead of applied.
joined = Counter("_".join(_corrected(m)) for m in PATTERN.finditer(text))
reports = []
def rewrite(match: re.Match) -> str:
suite, name = match["suite"], match["name"]
new_suite, new_name = _corrected(match)
line = text.count("\n", 0, match.start()) + 1
if match["macro"] in FIXTURE_MACROS and camel_case(suite) != suite:
reports.append(
f"{line}: fixture '{suite}' is not CamelCase: rename the class "
f"to '{camel_case(suite)}' by hand"
)
if (new_suite, new_name) == (suite, name):
return match[0]
if joined[f"{new_suite}_{new_name}"] > 1:
reports.append(
f"{line}: cannot rename '{suite}, {name}' to '{new_suite}, "
f"{new_name}': another test case already generates that name"
)
return match[0]
if new_suite != suite:
reports.append(f"{line}: renamed suite '{suite}' to '{new_suite}'")
if new_name != name:
reports.append(f"{line}: renamed test case '{name}' to '{new_name}'")
return match["head"] + new_suite + match["mid"] + new_name + match["tail"]
return PATTERN.sub(rewrite, text), reports
def fix_names(path: Path) -> bool:
"""Corrects one file's gtest names, reporting each on stdout."""
original = path.read_text(encoding="utf-8")
fixed, reports = fix_source(original)
for report in reports:
print(f"{path}:{report}")
if fixed != original:
path.write_text(fixed, encoding="utf-8")
return not reports
def main() -> int:
files = [Path(f) for f in sys.argv[1:]]
success = True
for path in files:
success &= fix_names(path)
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,259 +0,0 @@
#!/usr/bin/env python3
"""
Tests for fix_gtest_names.py.
Run directly (no test framework needed):
./bin/pre-commit/test_fix_gtest_names.py
or under pytest:
pytest bin/pre-commit/test_fix_gtest_names.py
"""
import sys
import textwrap
from fix_gtest_names import camel_case, fix_source, snake_case
def dedent(text: str) -> str:
"""Removes a fixture's common indentation and its leading newline.
Lets fixtures be written as indented triple-quoted here-docs while keeping
honest 1-based line numbers.
"""
return textwrap.dedent(text).lstrip("\n")
def fixed(text: str) -> str:
return fix_source(dedent(text))[0]
def reports(text: str) -> list[str]:
return fix_source(dedent(text))[1]
# --- conversion --------------------------------------------------------------
def test_snake_case_conversion() -> None:
assert snake_case("BadInputs") == "bad_inputs"
assert snake_case("mulDiv") == "mul_div"
assert snake_case("already_snake") == "already_snake"
assert snake_case("base64") == "base64"
def test_snake_case_keeps_acronyms_whole() -> None:
assert snake_case("SetAndResetAccountTxnID") == "set_and_reset_account_txn_id"
assert snake_case("XRPToIOU") == "xrp_to_iou"
assert snake_case("STAmountMath") == "st_amount_math"
def test_camel_case_conversion() -> None:
assert camel_case("json_value") == "JsonValue"
assert camel_case("mulDiv") == "MulDiv"
assert camel_case("scope") == "Scope"
assert camel_case("base64") == "Base64"
def test_camel_case_leaves_acronyms_alone() -> None:
# A snake_case round-trip would give `ShaMapTest` / `ParseStatmRsSkB` here.
assert camel_case("SHAMapTest") == "SHAMapTest"
assert camel_case("parseStatmRSSkB") == "ParseStatmRSSkB"
assert camel_case("XRPAmount") == "XRPAmount"
assert camel_case("CSPRNG") == "CSPRNG"
def test_disabled_prefix_preserved() -> None:
assert snake_case("DISABLED_FooBar") == "DISABLED_foo_bar"
assert snake_case("DISABLED_foo_bar") == "DISABLED_foo_bar"
assert snake_case("DISABLED_") == "DISABLED_"
assert camel_case("DISABLED_foo_bar") == "DISABLED_FooBar"
assert camel_case("DISABLED_") == "DISABLED_"
# --- what counts as a test definition ---------------------------------------
def test_all_macros_recognized() -> None:
code = """
TEST(Suite, oneName)
TEST_F(Fixture, twoName)
TEST_P(Fixture, threeName)
TYPED_TEST(Fixture, fourName)
TYPED_TEST_P(Fixture, fiveName)
"""
assert fixed(code) == dedent("""
TEST(Suite, one_name)
TEST_F(Fixture, two_name)
TEST_P(Fixture, three_name)
TYPED_TEST(Fixture, four_name)
TYPED_TEST_P(Fixture, five_name)
""")
def test_conforming_definitions_untouched() -> None:
code = """
TEST(AccountSet, bad_inputs)
TEST_F(MutexMakeTest, default_constructor)
TEST(SHAMap, DISABLED_slow_path)
"""
assert reports(code) == []
assert fixed(code) == dedent(code)
def test_lookalikes_ignored() -> None:
code = """
// TEST(Suite, notATest)
TEST_EXPECT(someCall())
TEST_EXPECTS(amount == value, amount.getText())
INSTANTIATE_TEST_SUITE_P(Prefix, Fixture, testValues());
auto x = TEST(Suite, notATest);
TYPED_TEST_SUITE(Fixture, MyTypes);
"""
assert reports(code) == []
assert fixed(code) == dedent(code)
def test_indented_and_wrapped_definitions() -> None:
code = """
namespace ripple {
TEST(Suite, indentedName)
}
TEST_F(
SomeVeryLongFixtureName,
wrappedName)
"""
assert fixed(code) == dedent("""
namespace ripple {
TEST(Suite, indented_name)
}
TEST_F(
SomeVeryLongFixtureName,
wrapped_name)
""")
# --- rewriting --------------------------------------------------------------
def test_only_the_two_names_are_rewritten() -> None:
code = """
TEST(mulDiv, mulDiv)
{
auto const mulDiv = 1; // mulDiv stays
}
"""
assert fixed(code) == dedent("""
TEST(MulDiv, mul_div)
{
auto const mulDiv = 1; // mulDiv stays
}
""")
def test_suite_name_camel_cased() -> None:
code = """
TEST(json_value, limits)
TEST(scope, ScopeExit)
"""
assert reports(code) == [
"1: renamed suite 'json_value' to 'JsonValue'",
"2: renamed suite 'scope' to 'Scope'",
"2: renamed test case 'ScopeExit' to 'scope_exit'",
]
assert fixed(code) == dedent("""
TEST(JsonValue, limits)
TEST(Scope, scope_exit)
""")
def test_fixture_reported_but_not_renamed() -> None:
# The first argument names a class, so only a human (or clang-tidy) can
# rename it; the test-case name is still fixed.
code = """
TEST_F(my_fixture, someTest)
"""
assert reports(code) == [
"1: fixture 'my_fixture' is not CamelCase: rename the class to "
"'MyFixture' by hand",
"1: renamed test case 'someTest' to 'some_test'",
]
assert fixed(code) == dedent("""
TEST_F(my_fixture, some_test)
""")
def test_reports_carry_line_numbers() -> None:
code = """
#include <foo.h>
TEST(Suite, firstName)
TEST(Suite, secondName)
"""
assert reports(code) == [
"3: renamed test case 'firstName' to 'first_name'",
"5: renamed test case 'secondName' to 'second_name'",
]
# --- collisions -------------------------------------------------------------
def test_collision_reported_and_not_applied() -> None:
# Both would define `Suite_mul_div_Test`.
code = """
TEST(Suite, mulDiv)
TEST(Suite, mul_div)
"""
assert reports(code) == [
"1: cannot rename 'Suite, mulDiv' to 'Suite, mul_div': another test "
"case already generates that name"
]
assert fixed(code) == dedent(code)
def test_collision_between_converging_suites() -> None:
# Both suites camel-case to `SuiteA`, so both would define
# `SuiteA_one_test_Test`.
code = """
TEST(SuiteA, oneTest)
TEST(Suite_a, one_test)
"""
assert [r.split(":")[1].strip() for r in reports(code)] == [
"cannot rename 'SuiteA, oneTest' to 'SuiteA, one_test'",
"cannot rename 'Suite_a, one_test' to 'SuiteA, one_test'",
]
assert fixed(code) == dedent(code)
def test_same_name_in_different_suites_is_not_a_collision() -> None:
code = """
TEST(SuiteOne, mulDiv)
TEST(SuiteTwo, mulDiv)
"""
assert fixed(code) == dedent("""
TEST(SuiteOne, mul_div)
TEST(SuiteTwo, mul_div)
""")
def main() -> int:
tests = sorted(
(name, fn)
for name, fn in globals().items()
if name.startswith("test_") and callable(fn)
)
failed = 0
for name, fn in tests:
try:
fn()
print(f"PASS {name}")
except AssertionError as exc:
failed += 1
print(f"FAIL {name}: {exc!r}")
print(f"\n{len(tests) - failed}/{len(tests)} passed")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -265,6 +265,13 @@ canWithdraw(
[[nodiscard]] TER
canWithdraw(ReadView const& view, STTx const& tx);
/**
* Pays out a withdrawal from a vault or loan broker pseudo-account.
*
* `sourceAmount` leaves `sourceAcct` and `destinationAmount` reaches `dstAcct`.
* The two differ only when the withdrawal carries a transfer fee; the
* difference is the fee and is settled through the issuer.
*/
[[nodiscard]] TER
doWithdraw(
ApplyViewContext ctx,
@@ -272,7 +279,8 @@ doWithdraw(
AccountID const& dstAcct,
AccountID const& sourceAcct,
XRPAmount priorBalance,
STAmount const& amount,
STAmount const& destinationAmount,
STAmount const& sourceAmount,
beast::Journal j);
/**

View File

@@ -316,6 +316,13 @@ transferRate(ReadView const& view, Asset const& asset);
[[nodiscard]] Rate
transferRate(ReadView const& view, STAmount const& amount);
/**
* Returns the amount delivered when the transfer fee is deducted from a fixed
* source amount.
*/
[[nodiscard]] STAmount
subtractTransferFee(STAmount const& sourceAmount, Rate const& rate);
//------------------------------------------------------------------------------
//
// Holding operations (Asset-based dispatchers)

View File

@@ -538,9 +538,14 @@ doWithdraw(
AccountID const& dstAcct,
AccountID const& sourceAcct,
XRPAmount priorBalance,
STAmount const& amount,
STAmount const& destinationAmount,
STAmount const& sourceAmount,
beast::Journal j)
{
XRPL_ASSERT(
destinationAmount.asset() == sourceAmount.asset(),
"xrpl::doWithdraw : delivered and source amounts use the same asset");
auto const dstSle = ctx.view.read(keylet::account(dstAcct));
// Create a trust line or MPToken for a self-destination only when there
@@ -550,9 +555,10 @@ doWithdraw(
// create+delete MPTokens in the same transaction.
if (dstAcct == senderAcct)
{
if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
if (destinationAmount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
{
if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
if (auto const ter =
addEmptyHolding(ctx, senderAcct, priorBalance, destinationAmount.asset(), j);
!isTesSuccess(ter) && ter != tecDUPLICATE)
return ter;
}
@@ -567,17 +573,33 @@ doWithdraw(
if (accountHolds(
ctx.view,
sourceAcct,
amount.asset(),
destinationAmount.asset(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j) < amount)
j) < sourceAmount)
{
// LCOV_EXCL_START
JLOG(j.error()) << "doWithdraw: negative balance of broker cover assets.";
JLOG(j.error()) << "doWithdraw: source account holds fewer assets than the withdrawal.";
return tefINTERNAL;
// LCOV_EXCL_STOP
}
// A transfer fee applies. Redeem the gross amount with the issuer, then
// issue the net amount to the destination.
if (sourceAmount != destinationAmount)
{
auto const issuer = destinationAmount.getIssuer();
XRPL_ASSERT(
!destinationAmount.native() && sourceAcct != issuer && dstAcct != issuer,
"xrpl::doWithdraw : transfer fee applies between token holders");
if (auto const ter = directSendNoFee(ctx.view, sourceAcct, issuer, sourceAmount, false, j);
!isTesSuccess(ter))
return ter;
return directSendNoFee(ctx.view, issuer, dstAcct, destinationAmount, false, j);
}
// A reserve sponsor only covers tx.Account's own objects, so resolve the
// sponsor against the destination. accountSend can auto-create a holding
// for dstAcct; keying on the destination ensures a third-party destination's
@@ -589,7 +611,7 @@ doWithdraw(
// Move the funds directly from the broker's pseudo-account to the
// dstAcct
return accountSend(
ctx.view, sourceAcct, dstAcct, amount, j, *sponsorSle, WaiveTransferFee::Yes);
ctx.view, sourceAcct, dstAcct, destinationAmount, j, *sponsorSle, WaiveTransferFee::Yes);
}
TER

View File

@@ -548,6 +548,12 @@ transferRate(ReadView const& view, STAmount const& amount)
return transferRate(view, amount.asset());
}
STAmount
subtractTransferFee(STAmount const& sourceAmount, Rate const& rate)
{
return divideRound(sourceAmount, rate, sourceAmount.asset(), false);
}
//------------------------------------------------------------------------------
//
// Holding operations

View File

@@ -7,6 +7,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -418,6 +419,7 @@ ValidVault::finalize(
{
bool const enforce = view.rules().enabled(featureSingleAssetVault);
bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
bool const fix350Enabled = view.rules().enabled(fixCleanup3_5_0);
if (!isTesSuccess(ret))
return true; // Do not perform checks
@@ -1144,6 +1146,17 @@ ValidVault::finalize(
auto const localPseudoDeltaAssets =
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
bool const feeAdjustedWithdrawal =
fix350Enabled && distinctDestination && !vaultAsset.native();
auto expectedDestinationDelta = localPseudoDeltaAssets * -1;
if (feeAdjustedWithdrawal)
{
expectedDestinationDelta =
subtractTransferFee(
STAmount{vaultAsset, expectedDestinationDelta},
transferRate(view, vaultAsset))
.value();
}
// For IOU assets near a precision boundary the destination's STAmount
// exponent can shift, making part of the sent value unrepresentable at
// the receiver's new scale — that portion is irreversibly absorbed by the
@@ -1152,7 +1165,8 @@ ValidVault::finalize(
// the destination's scale. Floor rounding is used so that values exactly
// at the step boundary are not mistakenly dismissed. Any representable
// discrepancy indicates a real accounting bug and must be caught.
auto const destroyedIsSubUlp = tolerateZeroDelta &&
auto const destroyedIsSubUlp = !feeAdjustedWithdrawal &&
tolerateZeroDelta &&
roundToAsset(
vaultAsset,
vaultDeltaAssets.delta * -1 - destinationDelta.delta,
@@ -1160,11 +1174,11 @@ ValidVault::finalize(
Number::RoundingMode::Downward) == kZero;
bool const withdrawAddsUp = fix340Enabled
? agreesWithinOneUnit(
localPseudoDeltaAssets * -1,
expectedDestinationDelta,
roundedDestinationDelta,
vaultAsset,
localMinScale)
: localPseudoDeltaAssets * -1 == roundedDestinationDelta;
: expectedDestinationDelta == roundedDestinationDelta;
if (!destroyedIsSubUlp && !withdrawAddsUp)
{
JLOG(j.fatal()) << "Invariant failed: " << //

View File

@@ -239,6 +239,7 @@ LoanBrokerCoverWithdraw::doApply()
brokerPseudoID,
preFeeBalance_,
amount,
amount,
j_);
}

View File

@@ -12,11 +12,13 @@
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
@@ -49,6 +51,24 @@ shouldWaiveWithdrawal(ReadView const& view, AccountID const& account, SLE::const
: WaiveUnrealizedLoss::No;
}
static Rate
withdrawalTransferRate(
ReadView const& view,
AccountID const& account,
AccountID const& destination,
Asset const& asset)
{
// Pre-fixCleanup3_5_0: every vault withdrawal waives the transfer fee.
// Post-fixCleanup3_5_0: the fee applies only when another token holder receives the assets.
if (!view.rules().enabled(fixCleanup3_5_0))
return kParityRate;
if (asset.native() || destination == account || destination == asset.getIssuer())
return kParityRate;
return transferRate(view, asset);
}
NotTEC
VaultWithdraw::preflight(PreflightContext const& ctx)
{
@@ -170,11 +190,13 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
if (!maybeAssets)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const rate = withdrawalTransferRate(ctx.view, account, dstAcct, vaultAsset);
auto const amountToReceive = subtractTransferFee(*maybeAssets, rate);
if (auto const ret = canWithdraw(
ctx.view,
account,
dstAcct,
*maybeAssets,
amountToReceive,
ctx.tx.isFieldPresent(sfDestinationTag),
ctx.tx[~sfCredentialIDs]))
return ret;
@@ -302,6 +324,8 @@ VaultWithdraw::doApply()
auto const amount = ctx_.tx[sfAmount];
Asset const vaultAsset = vault->at(sfAsset);
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
auto const rate = withdrawalTransferRate(view(), accountID_, dstAcct, vaultAsset);
MPTIssue const share{mptIssuanceID};
STAmount sharesRedeemed = {share};
@@ -329,8 +353,10 @@ VaultWithdraw::doApply()
auto const truncate =
view().rules().enabled(fixCleanup3_4_0) ? TruncateShares::Yes : TruncateShares::No;
{
auto const sourceAmount =
rate == kParityRate ? amount : multiplyRound(amount, rate, vaultAsset, true);
auto const maybeShares = assetsToSharesWithdraw(
vault, sleIssuance, amount, truncate, waiveUnrealizedLoss);
vault, sleIssuance, sourceAmount, truncate, waiveUnrealizedLoss);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesRedeemed = *maybeShares;
@@ -531,7 +557,17 @@ VaultWithdraw::doApply()
<< " assetsAvailable=" << allAvailable.getText();
}
assetsWithdrawn = allAvailable;
}
auto const assetsDelivered = subtractTransferFee(assetsWithdrawn, rate);
if (assetsWithdrawn > beast::kZero && assetsDelivered == beast::kZero)
{
JLOG(j_.debug()) << "VaultWithdraw: transfer fee reduces the payout to zero";
return tecPRECISION_LOSS;
}
if (isFinalWithdrawal)
{
// Do not let dust accumulate in the Vault.
assetsTotal = 0;
assetsAvailable = 0;
@@ -583,9 +619,15 @@ VaultWithdraw::doApply()
associateAsset(*vault, vaultAsset);
auto const dstAcct = ctx_.tx[~sfDestination].value_or(accountID_);
return doWithdraw(
applyViewContext, accountID_, dstAcct, vaultAccount, preFeeBalance_, assetsWithdrawn, j_);
applyViewContext,
accountID_,
dstAcct,
vaultAccount,
preFeeBalance_,
assetsDelivered,
assetsWithdrawn,
j_);
}
void

View File

@@ -50,6 +50,38 @@ namespace xrpl {
class VaultLifecycle_test : public VaultTestBase
{
private:
struct FundedVault
{
Keylet keylet;
test::jtx::Account account;
test::jtx::PrettyAsset shares;
};
/**
* Creates a vault owned by `owner` and deposits `amount` into it from `depositor`.
*/
static FundedVault
createFundedVault(
test::jtx::Env& env,
test::jtx::Vault& vault,
test::jtx::Account const& owner,
test::jtx::Account const& depositor,
STAmount const& amount)
{
auto [tx, keylet] = vault.create({.owner = owner, .asset = amount.asset()});
env(tx);
env.close();
env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount}));
env.close();
auto const sle = env.le(keylet);
test::jtx::Account const account{"vault", sle->at(sfAccount)};
env.memoize(account);
return {
.keylet = keylet,
.account = account,
.shares = test::jtx::PrettyAsset{MPTIssue{sle->at(sfShareMPTID)}}};
}
void
testSequences()
{
@@ -569,6 +601,7 @@ private:
bool enableClawback = true;
bool requireAuth = true;
int initialXRP = 1000;
std::uint16_t transferFee = 0;
FeatureBitset features = testableAmendments();
};
@@ -593,7 +626,8 @@ private:
MPTTester mptt{env, issuer, kMptInitNoFund};
auto const kNone = LedgerSpecificFlags(0);
mptt.create(
{.flags = tfMPTCanTransfer | tfMPTCanLock |
{.transferFee = args.transferFee,
.flags = tfMPTCanTransfer | tfMPTCanLock |
(args.enableClawback ? tfMPTCanClawback : kNone) |
(args.requireAuth ? tfMPTRequireAuth : kNone)});
PrettyAsset const asset = mptt.issuanceID();
@@ -611,6 +645,230 @@ private:
test(env, issuer, owner, depositor, asset, vault, mptt);
};
// The MPT charges a 25% transfer fee, so 100 gross delivers 80 net.
auto const feeArgs = CaseArgs{.transferFee = 25'000};
auto const testTransferFeeGate = [&](FeatureBitset const& features) {
testCase(
[this, features](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
bool const feeCharged = features[fixCleanup3_5_0];
testcase(
feeCharged ? "MPT transfer fee on third-party withdrawal"
: "MPT transfer fee waived pre-fixCleanup3_5_0");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto tx = vault.withdraw(
{.depositor = depositor,
.id = funded.keylet.key,
.amount = funded.shares(100)});
tx[sfDestination] = owner.human();
env(tx);
env.close();
auto const expected = feeCharged ? 80 : 100;
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 900));
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, expected));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(900 + expected));
BEAST_EXPECT(
env.balance(funded.account, asset.raw().get<MPTIssue>()) == asset(0));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env.close();
},
CaseArgs{.transferFee = 25'000, .features = features});
};
testTransferFeeGate(testableAmendments() - fixCleanup3_5_0);
testTransferFeeGate(testableAmendments());
testCase(
[this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT transfer fee on fixed-asset third-party withdrawal");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(40)});
tx[sfDestination] = owner.human();
env(tx);
env.close();
// The destination receives the requested 40; the gross 50 leaves the vault.
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 40));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(50));
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(50));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(990));
},
feeArgs);
testCase(
[this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT transfer fee on fixed-share third-party withdrawal");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = funded.shares(25)});
tx[sfDestination] = owner.human();
env(tx);
env.close();
// The 25 shares redeem 25 assets; the destination receives 20 net.
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 20));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(75));
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(75));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(995));
},
feeArgs);
testCase(
[this](
Env& env,
Account const& issuer,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT no transfer fee on withdrawal to issuer");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(10)});
tx[sfDestination] = issuer.human();
env(tx);
env.close();
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(90));
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(90));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(990));
},
feeArgs);
testCase(
[this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT no transfer fee on self-withdrawal");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
env(vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(10)}));
env.close();
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 910));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(90));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(1000));
},
feeArgs);
testCase(
[this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT transfer fee must fit in the vault position");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(100));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
// Delivering 81 costs 102 gross, more than the 100 shares held.
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(81)});
tx[sfDestination] = owner.human();
env(tx, Ter{tecINSUFFICIENT_FUNDS});
env.close();
// Delivering 80 costs exactly the 100 shares held.
tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = asset(80)});
tx[sfDestination] = owner.human();
env(tx);
env.close();
BEAST_EXPECT(mptt.checkMPTokenAmount(owner, 80));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(0));
BEAST_EXPECT(env.balance(depositor, shareIssue) == funded.shares(0));
BEAST_EXPECT(mptt.checkMPTokenOutstandingAmount(980));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env.close();
},
feeArgs);
testCase(
[this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT transfer fee rounds the payout down to zero");
auto const funded = createFundedVault(env, vault, owner, depositor, asset(1));
auto const mptIssue = asset.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = funded.shares(1)});
tx[sfDestination] = owner.human();
env(tx, Ter{tecPRECISION_LOSS});
env.close();
// A self-withdrawal pays no fee, so the same share still redeems.
env(vault.withdraw(
{.depositor = depositor, .id = funded.keylet.key, .amount = funded.shares(1)}));
env.close();
BEAST_EXPECT(mptt.checkMPTokenAmount(depositor, 1000));
BEAST_EXPECT(env.balance(funded.account, mptIssue) == asset(0));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env.close();
},
feeArgs);
testCase([this](
Env& env,
Account const& issuer,
@@ -1487,66 +1745,251 @@ private:
env.close();
});
// The issuer charges a 25% transfer fee, so 100 gross delivers 80 net.
auto const feeArgs = CaseArgs{.transferRate = 1.25};
auto const testTransferFeeGate = [&](FeatureBitset const& features) {
testCase(
[this, features](
Env& env,
Account const& owner,
Account const& issuer,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
bool const feeCharged = features[fixCleanup3_5_0];
testcase(
feeCharged ? "IOU transfer fee on third-party withdrawal"
: "IOU transfer fee waived pre-fixCleanup3_5_0");
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
// Deposits are fee-free.
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(100));
env(vault.clawback(
{.issuer = issuer,
.id = funded.keylet.key,
.holder = owner,
.amount = asset(50)}));
env.close();
// Clawbacks are fee-free.
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
env(vault.withdraw(
{.depositor = owner,
.id = funded.keylet.key,
.amount = funded.shares(20'000'000)}));
env.close();
// Self-withdrawals are fee-free.
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(30));
auto tx = vault.withdraw(
{.depositor = owner,
.id = funded.keylet.key,
.amount = funded.shares(30'000'000)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
auto const expected = feeCharged ? asset(24) : asset(30);
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
BEAST_EXPECT(env.balance(charlie, issue) == expected);
BEAST_EXPECT(env.balance(funded.account, issue) == asset(0));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env.close();
},
CaseArgs{.transferRate = 1.25, .features = features});
};
testTransferFeeGate(testableAmendments() - fixCleanup3_5_0);
testTransferFeeGate(testableAmendments());
testCase(
[&, this](
[this](
Env& env,
Account const& owner,
Account const& issuer,
Account const&,
Account const& charlie,
auto vaultAccount,
auto,
Vault& vault,
PrettyAsset const& asset,
auto issuanceId) {
testcase("IOU transfer fees not applied");
auto&&...) {
testcase("IOU transfer fee on fixed-asset third-party withdrawal");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(40)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
// The destination receives the requested 40; the gross 50 leaves the vault.
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(50'000'000));
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
},
feeArgs);
testCase(
[this](
Env& env,
Account const& owner,
Account const&,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU transfer fee on fixed-share third-party withdrawal");
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
auto const shareIssue = funded.shares.raw().get<MPTIssue>();
auto tx = vault.withdraw(
{.depositor = owner,
.id = funded.keylet.key,
.amount = funded.shares(25'000'000)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
// The shares redeem 25 assets; the destination receives 20 net.
BEAST_EXPECT(env.balance(charlie, issue) == asset(20));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(75));
BEAST_EXPECT(env.balance(owner, shareIssue) == funded.shares(75'000'000));
},
feeArgs);
testCase(
[this](
Env& env,
Account const& owner,
Account const& issuer,
Account const&,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU no transfer fee on withdrawal to issuer");
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
Asset const share = Asset(issuanceId(keylet));
// transfer fees ignored on deposit
auto tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(10)});
tx[sfDestination] = issuer.human();
env(tx);
env.close();
BEAST_EXPECT(env.balance(funded.account, issue) == asset(90));
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(100));
},
feeArgs);
{
auto tx = vault.clawback(
{.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
env(tx);
env.close();
}
testCase(
[this](
Env& env,
Account const& owner,
Account const&,
Account const&,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU no transfer fee on self-withdrawal");
// transfer fees ignored on clawback
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(50));
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
env(vault.withdraw(
{.depositor = owner, .id = keylet.key, .amount = share(20'000'000)}));
{.depositor = owner, .id = funded.keylet.key, .amount = asset(10)}));
env.close();
// transfer fees ignored on withdraw
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(30));
BEAST_EXPECT(env.balance(owner, issue) == asset(110));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(90));
},
feeArgs);
{
auto tx = vault.withdraw(
{.depositor = owner, .id = keylet.key, .amount = share(30'000'000)});
tx[sfDestination] = charlie.human();
env(tx);
}
testCase(
[this](
Env& env,
Account const& owner,
Account const&,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU transfer fee must fit in the vault position");
// transfer fees ignored on withdraw to 3rd party
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
BEAST_EXPECT(env.balance(charlie, issue) == asset(30));
BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(0));
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
env(vault.del({.owner = owner, .id = keylet.key}));
// Delivering 81 costs 101.25 gross, more than the vault position.
auto tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(81)});
tx[sfDestination] = charlie.human();
env(tx, Ter{tecINSUFFICIENT_FUNDS});
env.close();
// Delivering 80 costs exactly the 100 held.
tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(80)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
BEAST_EXPECT(env.balance(charlie, issue) == asset(80));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(0));
env(vault.del({.owner = owner, .id = funded.keylet.key}));
env.close();
},
CaseArgs{.transferRate = 1.25});
feeArgs);
testCase(
[this](
Env& env,
Account const& owner,
Account const&,
Account const& charlie,
auto,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU receiver limit applies to the net amount");
// The limit fits the 40 delivered but not the 50 gross.
env(trust(charlie, asset(40)));
env.close();
auto const funded = createFundedVault(env, vault, owner, owner, asset(100));
auto const issue = asset.raw().get<Issue>();
auto tx = vault.withdraw(
{.depositor = owner, .id = funded.keylet.key, .amount = asset(40)});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
BEAST_EXPECT(env.balance(charlie, issue) == asset(40));
BEAST_EXPECT(env.balance(funded.account, issue) == asset(50));
},
feeArgs);
testCase([&, this](
Env& env,

View File

@@ -49,7 +49,7 @@ TEST(MallocTrimReport, structure)
}
#if defined(__GLIBC__) && BOOST_OS_LINUX
TEST(ParseStatmRSSkB, standard_format)
TEST(parseStatmRSSkB, standard_format)
{
using xrpl::detail::parseStatmRSSkB;
@@ -121,7 +121,7 @@ TEST(ParseStatmRSSkB, standard_format)
}
#endif
TEST(MallocTrim, without_debug_logging)
TEST(mallocTrim, without_debug_logging)
{
beast::Journal const journal{beast::Journal::getNullSink()};
@@ -144,7 +144,7 @@ TEST(MallocTrim, without_debug_logging)
#endif
}
TEST(MallocTrim, empty_tag)
TEST(mallocTrim, empty_tag)
{
beast::Journal const journal{beast::Journal::getNullSink()};
MallocTrimReport const report = mallocTrim("", journal);
@@ -157,7 +157,7 @@ TEST(MallocTrim, empty_tag)
#endif
}
TEST(MallocTrim, with_debug_logging)
TEST(mallocTrim, with_debug_logging)
{
struct DebugSink : public beast::Journal::Sink
{
@@ -194,7 +194,7 @@ TEST(MallocTrim, with_debug_logging)
#endif
}
TEST(MallocTrim, repeated_calls)
TEST(mallocTrim, repeated_calls)
{
beast::Journal const journal{beast::Journal::getNullSink()};

View File

@@ -10,7 +10,7 @@
using namespace xrpl;
TEST(RangeSet, prev_missing)
TEST(RangeSet, prevMissing)
{
// Set will include:
// [ 0, 5]
@@ -36,7 +36,7 @@ TEST(RangeSet, prev_missing)
}
}
TEST(RangeSet, to_string)
TEST(RangeSet, toString)
{
RangeSet<std::uint32_t> set;
EXPECT_EQ(to_string(set), "empty");
@@ -54,7 +54,7 @@ TEST(RangeSet, to_string)
EXPECT_EQ(to_string(set), "1-2,6");
}
TEST(RangeSet, from_string)
TEST(RangeSet, fromString)
{
RangeSet<std::uint32_t> set;

View File

@@ -290,7 +290,7 @@ TEST_F(StringUtilitiesTest, to_string)
EXPECT_EQ(result, "hello");
}
TEST_F(StringUtilitiesTest, trim_whitespace)
TEST_F(StringUtilitiesTest, trimWhitespace)
{
EXPECT_EQ(trimWhitespace(""), "");
EXPECT_EQ(trimWhitespace(" "), "");
@@ -303,7 +303,7 @@ TEST_F(StringUtilitiesTest, trim_whitespace)
EXPECT_EQ(trimWhitespace(" a b\tc "), "a b\tc");
}
TEST_F(StringUtilitiesTest, to_lower)
TEST_F(StringUtilitiesTest, toLower)
{
EXPECT_EQ(toLower(""), "");
EXPECT_EQ(toLower("ABC"), "abc");
@@ -318,7 +318,7 @@ TEST_F(StringUtilitiesTest, to_lower)
// Both helpers are documented as depending only on their input. Guard that by
// checking the bytes just outside ASCII, which a locale-aware isspace/tolower
// could classify differently.
TEST_F(StringUtilitiesTest, trim_and_lower_ignore_locale)
TEST_F(StringUtilitiesTest, trimAndLowerIgnoreLocale)
{
// 0xA0 is NO-BREAK SPACE in Latin-1 and is whitespace to some locales.
std::string const nbsp("\xA0", 1);

View File

@@ -14,7 +14,7 @@ check(std::string const& in, std::string const& out)
EXPECT_EQ(base64Decode(encoded), in);
}
TEST(Base64, base64)
TEST(base64, base64)
{
// cspell: disable
check("", "");

View File

@@ -128,7 +128,7 @@ struct BaseUintTest : public ::testing::Test
using BaseUintDeathTest = BaseUintTest;
TEST_F(BaseUintDeathTest, from_raw_size_mismatch)
TEST_F(BaseUintDeathTest, fromRaw_size_mismatch)
{
// ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts. Rather than twist
// these tests into knots to make them work, just skip them.

View File

@@ -6,7 +6,7 @@
using namespace xrpl;
TEST(Contract, contract)
TEST(contract, contract)
{
try
{

View File

@@ -7,7 +7,7 @@
using namespace xrpl;
TEST(MulDiv, mul_div)
TEST(mulDiv, mulDiv)
{
auto const max = std::numeric_limits<std::uint64_t>::max();
std::uint64_t const max32 = std::numeric_limits<std::uint32_t>::max();

View File

@@ -6,7 +6,7 @@
using namespace xrpl;
TEST(Scope, scope_exit)
TEST(scope, ScopeExit)
{
// ScopeExit always executes the functor on destruction,
// unless release() is called
@@ -56,7 +56,7 @@ TEST(Scope, scope_exit)
EXPECT_EQ(i, 5);
}
TEST(Scope, scope_fail)
TEST(scope, ScopeFail)
{
// ScopeFail executes the functor on destruction only
// if an exception is unwinding, unless release() is called
@@ -106,7 +106,7 @@ TEST(Scope, scope_fail)
EXPECT_EQ(i, 5);
}
TEST(Scope, scope_success)
TEST(scope, ScopeSuccess)
{
// ScopeSuccess executes the functor on destruction only
// if an exception is not unwinding, unless release() is called

View File

@@ -105,7 +105,7 @@ static_assert(
using TagInt = TaggedInteger<std::int32_t, Tag1>;
TEST(TaggedInteger, comparison_operators)
TEST(tagged_integer, comparison_operators)
{
TagInt const zero(0);
TagInt const one(1);
@@ -131,7 +131,7 @@ TEST(TaggedInteger, comparison_operators)
EXPECT_FALSE(one <= zero);
}
TEST(TaggedInteger, increment_decrement_operators)
TEST(tagged_integer, increment_decrement_operators)
{
TagInt const zero(0);
TagInt const one(1);
@@ -146,7 +146,7 @@ TEST(TaggedInteger, increment_decrement_operators)
EXPECT_EQ(a, zero);
}
TEST(TaggedInteger, arithmetic_operators)
TEST(tagged_integer, arithmetic_operators)
{
TagInt const a{-2};
EXPECT_EQ(+a, TagInt{-2});
@@ -166,7 +166,7 @@ TEST(TaggedInteger, arithmetic_operators)
EXPECT_EQ((TagInt{16} >> TagInt{2}), TagInt{4});
}
TEST(TaggedInteger, assignment_operators)
TEST(tagged_integer, assignment_operators)
{
TagInt a{-2};
TagInt b{0};

View File

@@ -6,7 +6,7 @@
using namespace xrpl;
TEST(Csprng, get_values)
TEST(csprng, get_values)
{
auto& engine = cryptoPrng();
auto randVal = engine();

View File

@@ -21,7 +21,7 @@
namespace xrpl {
TEST(JsonValue, limits)
TEST(json_value, limits)
{
using namespace json;
static_assert(Value::kMinInt == Int(~(UInt(-1) / 2)));
@@ -29,7 +29,7 @@ TEST(JsonValue, limits)
static_assert(Value::kMaxUInt == UInt(-1));
}
TEST(JsonValue, construct_and_compare_json_static_string)
TEST(json_value, construct_and_compare_Json_StaticString)
{
static constexpr char kSample[]{"Contents of a json::StaticString"};
@@ -52,7 +52,7 @@ TEST(JsonValue, construct_and_compare_json_static_string)
EXPECT_NE(kTest3, str);
}
TEST(JsonValue, different_types)
TEST(json_value, different_types)
{
// Exercise ValueType constructor
static constexpr json::StaticString kStaticStr{"staticStr"};
@@ -206,7 +206,7 @@ TEST(JsonValue, different_types)
}
}
TEST(JsonValue, compare_strings)
TEST(json_value, compare_strings)
{
auto doCompare = [&](json::Value const& lhs,
json::Value const& rhs,
@@ -560,7 +560,7 @@ TEST(JsonValue, compare_strings)
#pragma pop_macro("DO_COMPARE")
}
TEST(JsonValue, bool)
TEST(json_value, bool)
{
EXPECT_FALSE(json::Value());
@@ -583,7 +583,7 @@ TEST(JsonValue, bool)
EXPECT_TRUE(bool(object));
}
TEST(JsonValue, bad_json)
TEST(json_value, bad_json)
{
char const* s(R"({"method":"ledger","params":[{"ledger_index":1e300}]})");
@@ -607,7 +607,7 @@ parseValue(std::string const& doc)
} // namespace
TEST(JsonValue, parse_double_valid)
TEST(json_value, parse_double_valid)
{
// 1e300 is large but still representable, so it parses (unlike the out-of-range cases below).
for (auto const& [text, expected] :
@@ -627,14 +627,14 @@ TEST(JsonValue, parse_double_valid)
}
}
TEST(JsonValue, parse_double_out_of_range)
TEST(json_value, parse_double_out_of_range)
{
// Magnitudes with no finite double representation are rejected.
for (char const* oor : {"1e400", "-1e400", "0.001e500", "1e-400", "-1e-400", "123e-500"})
EXPECT_FALSE(parseValue(oor).has_value()) << oor;
}
TEST(JsonValue, parse_double_malformed)
TEST(json_value, parse_double_malformed)
{
// readNumber() collects any run of digits and '.eE+-' into a single Double
// token, so these malformed tokens reach decodeDouble. Each has a valid
@@ -644,7 +644,7 @@ TEST(JsonValue, parse_double_malformed)
EXPECT_FALSE(parseValue(bad).has_value()) << bad;
}
TEST(JsonValue, edge_cases)
TEST(json_value, edge_cases)
{
std::uint32_t const maxUInt = std::numeric_limits<std::uint32_t>::max();
std::int32_t const maxInt = std::numeric_limits<std::int32_t>::max();
@@ -791,7 +791,7 @@ TEST(JsonValue, edge_cases)
}
}
TEST(JsonValue, copy)
TEST(json_value, copy)
{
json::Value v1{2.5};
EXPECT_TRUE(v1.isDouble());
@@ -812,7 +812,7 @@ TEST(JsonValue, copy)
EXPECT_EQ(v1, v2);
}
TEST(JsonValue, move)
TEST(json_value, move)
{
json::Value v1{2.5};
EXPECT_TRUE(v1.isDouble());
@@ -831,7 +831,7 @@ TEST(JsonValue, move)
EXPECT_NE(v1, v2); // NOLINT(bugprone-use-after-move)
}
TEST(JsonValue, comparisons)
TEST(json_value, comparisons)
{
json::Value a, b;
auto testEquals = [&](std::string const& name) {
@@ -886,7 +886,7 @@ TEST(JsonValue, comparisons)
testGreaterThan("big");
}
TEST(JsonValue, compact)
TEST(json_value, compact)
{
json::Value j;
json::Reader r;
@@ -909,7 +909,7 @@ TEST(JsonValue, compact)
}
}
TEST(JsonValue, conversions)
TEST(json_value, conversions)
{
// We have json::ValueType::Real but json::Value::asDouble.
// TODO: What's the thinking here?
@@ -1125,7 +1125,7 @@ TEST(JsonValue, conversions)
}
}
TEST(JsonValue, access_members)
TEST(json_value, access_members)
{
json::Value val;
EXPECT_EQ(val.type(), json::ValueType::Null);
@@ -1218,7 +1218,7 @@ TEST(JsonValue, access_members)
}
}
TEST(JsonValue, remove_members)
TEST(json_value, remove_members)
{
json::Value val;
EXPECT_EQ(val.removeMember(std::string("member")).type(), json::ValueType::Null);
@@ -1245,7 +1245,7 @@ TEST(JsonValue, remove_members)
EXPECT_EQ(val.size(), 0);
}
TEST(JsonValue, iterator)
TEST(json_value, iterator)
{
{
// Iterating an array.
@@ -1331,7 +1331,7 @@ TEST(JsonValue, iterator)
}
}
TEST(JsonValue, nest_limits)
TEST(json_value, nest_limits)
{
json::Reader r;
{
@@ -1377,7 +1377,7 @@ TEST(JsonValue, nest_limits)
}
}
TEST(JsonValue, memory_leak)
TEST(json_value, memory_leak)
{
// When run with the address sanitizer, this test confirms there is no
// memory leak with the scenarios below.

View File

@@ -10,7 +10,7 @@
namespace xrpl::test {
TEST(AMMEntryTests, constructors)
TEST(AMMEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(AccountRootEntryTests, constructors)
TEST(AccountRootEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -7,7 +7,7 @@
namespace xrpl::test {
TEST(AmendmentsEntryTests, constructors)
TEST(AmendmentsEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -11,7 +11,7 @@
namespace xrpl::test {
TEST(BridgeEntryTests, constructors)
TEST(BridgeEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(CheckEntryTests, constructors)
TEST(CheckEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -11,7 +11,7 @@
namespace xrpl::test {
TEST(CredentialEntryTests, constructors)
TEST(CredentialEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -7,7 +7,7 @@
namespace xrpl::test {
TEST(DIDEntryTests, constructors)
TEST(DIDEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(DelegateEntryTests, constructors)
TEST(DelegateEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -14,7 +14,7 @@
namespace xrpl::test {
TEST(DepositPreauthEntryTests, constructors)
TEST(DepositPreauthEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -10,7 +10,7 @@
namespace xrpl::test {
TEST(DirectoryNodeEntryTests, constructors)
TEST(DirectoryNodeEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(EscrowEntryTests, constructors)
TEST(EscrowEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -7,7 +7,7 @@
namespace xrpl::test {
TEST(FeeSettingsEntryTests, constructors)
TEST(FeeSettingsEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -7,7 +7,7 @@
namespace xrpl::test {
TEST(LedgerHashesEntryTests, constructors)
TEST(LedgerHashesEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(LoanBrokerEntryTests, constructors)
TEST(LoanBrokerEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -9,7 +9,7 @@
namespace xrpl::test {
TEST(LoanEntryTests, constructors)
TEST(LoanEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(MPTokenEntryTests, constructors)
TEST(MPTokenEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -10,7 +10,7 @@
namespace xrpl::test {
TEST(MPTokenIssuanceEntryTests, constructors)
TEST(MPTokenIssuanceEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(NFTokenOfferEntryTests, constructors)
TEST(NFTokenOfferEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(NFTokenPageEntryTests, constructors)
TEST(NFTokenPageEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -7,7 +7,7 @@
namespace xrpl::test {
TEST(NegativeUNLEntryTests, constructors)
TEST(NegativeUNLEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(OfferEntryTests, constructors)
TEST(OfferEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -9,7 +9,7 @@
namespace xrpl::test {
TEST(OracleEntryTests, constructors)
TEST(OracleEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -9,7 +9,7 @@
namespace xrpl::test {
TEST(PayChannelEntryTests, constructors)
TEST(PayChannelEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(PermissionedDomainEntryTests, constructors)
TEST(PermissionedDomainEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -10,7 +10,7 @@
namespace xrpl::test {
TEST(RippleStateEntryTests, constructors)
TEST(RippleStateEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -149,7 +149,7 @@ protected:
}
};
TEST_F(SLEBaseTests, read_only)
TEST_F(SLEBaseTests, ReadOnly)
{
AccountRootEntryR const absent(bob_.id(), env_.getClosedLedger());
EXPECT_FALSE(absent.exists());
@@ -168,7 +168,7 @@ TEST_F(SLEBaseTests, read_only)
EXPECT_EQ(&present.readView(), &env_.getClosedLedger());
}
TEST_F(SLEBaseTests, adopt_sle)
TEST_F(SLEBaseTests, AdoptSLE)
{
auto const sle = env_.getClosedLedger().read(keylet::account(alice_.id()));
ASSERT_NE(sle, nullptr);
@@ -201,7 +201,7 @@ TEST_F(SLEBaseTests, adopt_sle)
"writable entries must not be constructible from a bare SLE");
}
TEST_F(SLEBaseTests, writable_accessors)
TEST_F(SLEBaseTests, WritableAccessors)
{
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);
beast::Journal const j{beast::Journal::getNullSink()};
@@ -236,7 +236,7 @@ TEST_F(SLEBaseTests, writable_accessors)
!HasApplyView<AccountRootEntryR>, "applyView() must not exist on a read-only entry");
}
TEST_F(SLEBaseTests, apply_view_context_ctor)
TEST_F(SLEBaseTests, ApplyViewContextCtor)
{
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);
beast::Journal const j{beast::Journal::getNullSink()};
@@ -260,7 +260,7 @@ TEST_F(SLEBaseTests, apply_view_context_ctor)
EXPECT_EQ(fromCtx.rawSle(), fromView.rawSle());
}
TEST_F(SLEBaseTests, writable_lifecycle)
TEST_F(SLEBaseTests, WritableLifecycle)
{
// A view we never apply, so nothing here reaches the ledger.
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);
@@ -318,7 +318,7 @@ TEST_F(SLEBaseTests, writable_lifecycle)
}
}
TEST_F(SLEBaseTests, conversion)
TEST_F(SLEBaseTests, Conversion)
{
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);
@@ -336,7 +336,7 @@ TEST_F(SLEBaseTests, conversion)
EXPECT_EQ(generic.type(), ltACCOUNT_ROOT);
}
TEST_F(SLEBaseTests, resolve_entry_peeks)
TEST_F(SLEBaseTests, ResolveEntryPeeks)
{
// getOpenLedger() is an OpenView, which derives from ReadView but not
// from ApplyView, so resolveEntry's dynamic_cast fails and this takes
@@ -368,7 +368,7 @@ TEST_F(SLEBaseTests, resolve_entry_peeks)
EXPECT_EQ(readOnly->getFieldU32(sfSequence), bumped);
}
TEST_F(SLEBaseTests, throws_on_missing_entry)
TEST_F(SLEBaseTests, ThrowsOnMissingEntry)
{
// A generic read-only entry has no static type to fall back on, so
// type() must read it off the (absent) SLE and throw.
@@ -389,7 +389,7 @@ TEST_F(SLEBaseTests, throws_on_missing_entry)
EXPECT_THROW(std::ignore = (*missing).getType(), std::logic_error);
}
TEST_F(SLEBaseTests, throws_on_missing_writable_entry)
TEST_F(SLEBaseTests, ThrowsOnMissingWritableEntry)
{
// A view we never apply, so nothing here reaches the ledger.
ApplyViewImpl av(&env_.getClosedLedger(), TapNone);

View File

@@ -7,7 +7,7 @@
namespace xrpl::test {
TEST(SignerListEntryTests, constructors)
TEST(SignerListEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(SponsorshipEntryTests, constructors)
TEST(SponsorshipEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(TicketEntryTests, constructors)
TEST(TicketEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -8,7 +8,7 @@
namespace xrpl::test {
TEST(VaultEntryTests, constructors)
TEST(VaultEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -12,7 +12,7 @@
namespace xrpl::test {
TEST(XChainOwnedClaimIDEntryTests, constructors)
TEST(XChainOwnedClaimIDEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -13,7 +13,7 @@
namespace xrpl::test {
TEST(XChainOwnedCreateAccountClaimIDEntryTests, constructors)
TEST(XChainOwnedCreateAccountClaimIDEntryTests, Constructors)
{
EntryTestEnv e;

View File

@@ -28,7 +28,7 @@ account(std::string_view hex)
// getText() builds its string from eight substitutions of the same type, so a
// transposed pair would still compile and still type check. Pin the output so
// the field/value pairing is actually verified.
TEST(STXChainBridge, get_text_pairs_each_field_with_its_value)
TEST(STXChainBridge, getTextPairsEachFieldWithItsValue)
{
auto const lockingDoor = account("0102030405060708090A0B0C0D0E0F1011121314");
auto const issuingDoor = account("14131211100F0E0D0C0B0A090807060504030201");
@@ -46,7 +46,7 @@ TEST(STXChainBridge, get_text_pairs_each_field_with_its_value)
EXPECT_EQ(bridge.getText(), expected);
}
TEST(STXChainBridge, get_text_on_a_default_bridge)
TEST(STXChainBridge, getTextOnADefaultBridge)
{
STXChainBridge const bridge;
auto const text = bridge.getText();

View File

@@ -12,7 +12,7 @@ using namespace xrpl;
// by subscribing the real cap through a WebSocket, which would exceed the frame
// limit and drop the connection before the check runs) lets the boundary be
// asserted exactly.
TEST(InfoSubSubscriptionCap, boundary)
TEST(InfoSubSubscriptionCap, Boundary)
{
constexpr std::size_t cap = kMaxSubscriptionsPerConnection;
@@ -30,7 +30,7 @@ TEST(InfoSubSubscriptionCap, boundary)
EXPECT_TRUE(exceedsSubscriptionCap(cap - 1, 2));
}
TEST(InfoSubSubscriptionCap, no_overflow)
TEST(InfoSubSubscriptionCap, NoOverflow)
{
constexpr std::size_t cap = kMaxSubscriptionsPerConnection;
constexpr std::size_t max = std::numeric_limits<std::size_t>::max();
@@ -41,7 +41,7 @@ TEST(InfoSubSubscriptionCap, no_overflow)
EXPECT_TRUE(exceedsSubscriptionCap(cap, max));
}
TEST(InfoSubSubscriptionCap, explicit_cap)
TEST(InfoSubSubscriptionCap, ExplicitCap)
{
// A configured override is honored: the boundary tracks the passed cap, not
// the built-in default. This is the seam doSubscribe uses to enforce a

View File

@@ -43,7 +43,7 @@
namespace xrpl::test {
TEST(AccountSet, null_account_set)
TEST(AccountSet, NullAccountSet)
{
TxTest env;
@@ -60,7 +60,7 @@ TEST(AccountSet, null_account_set)
EXPECT_EQ(accountRoot.getFlags(), 0);
}
TEST(AccountSet, most_flags)
TEST(AccountSet, MostFlags)
{
Account const alice("alice");
@@ -175,7 +175,7 @@ TEST(AccountSet, most_flags)
});
}
TEST(AccountSet, set_and_reset_account_txn_id)
TEST(AccountSet, SetAndResetAccountTxnID)
{
TxTest env;
Account const alice("alice");
@@ -206,7 +206,7 @@ TEST(AccountSet, set_and_reset_account_txn_id)
EXPECT_EQ(nowFlags, origFlags);
}
TEST(AccountSet, set_no_freeze)
TEST(AccountSet, SetNoFreeze)
{
TxTest env;
Account const alice("alice");
@@ -249,7 +249,7 @@ TEST(AccountSet, set_no_freeze)
EXPECT_TRUE(env.getAccountRoot(alice).isFlag(lsfNoFreeze));
}
TEST(AccountSet, domain)
TEST(AccountSet, Domain)
{
TxTest env;
Account const alice("alice");
@@ -317,7 +317,7 @@ TEST(AccountSet, domain)
}
}
TEST(AccountSet, message_key)
TEST(AccountSet, MessageKey)
{
TxTest env;
Account const alice("alice");
@@ -358,7 +358,7 @@ TEST(AccountSet, message_key)
telBAD_PUBLIC_KEY);
}
TEST(AccountSet, wallet_id)
TEST(AccountSet, WalletID)
{
TxTest env;
Account const alice("alice");
@@ -391,7 +391,7 @@ TEST(AccountSet, wallet_id)
EXPECT_FALSE(env.getAccountRoot(alice).hasWalletLocator());
}
TEST(AccountSet, email_hash)
TEST(AccountSet, EmailHash)
{
TxTest env;
Account const alice("alice");
@@ -422,7 +422,7 @@ TEST(AccountSet, email_hash)
EXPECT_FALSE(env.getAccountRoot(alice).hasEmailHash());
}
TEST(AccountSet, transfer_rate)
TEST(AccountSet, TransferRate)
{
struct TestCase
{
@@ -473,7 +473,7 @@ TEST(AccountSet, transfer_rate)
}
}
TEST(AccountSet, bad_inputs)
TEST(AccountSet, BadInputs)
{
TxTest env;
Account const alice("alice");
@@ -553,7 +553,7 @@ TEST(AccountSet, bad_inputs)
tecNO_ALTERNATIVE_KEY);
}
TEST(AccountSet, require_auth_with_dir)
TEST(AccountSet, RequireAuthWithDir)
{
TxTest env;
Account const alice("alice");
@@ -601,7 +601,7 @@ TEST(AccountSet, require_auth_with_dir)
tesSUCCESS);
}
TEST(AccountSet, ticket)
TEST(AccountSet, Ticket)
{
TxTest env;
Account const alice("alice");
@@ -660,7 +660,7 @@ TEST(AccountSet, ticket)
tefNO_TICKET);
}
TEST(AccountSet, bad_signing_key)
TEST(AccountSet, BadSigningKey)
{
TxTest env;
Account const alice("alice");
@@ -684,7 +684,7 @@ TEST(AccountSet, bad_signing_key)
EXPECT_FALSE(result.applied);
}
TEST(AccountSet, gateway)
TEST(AccountSet, Gateway)
{
Account const alice("alice");
Account const bob("bob");