Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66

* XRPLF/develop:
  ci: Use commit hash so workflows are not canceled when merging multiple PRs (5950)
  ci: Only upload codecov reports in the original repo, not in forks (5953)
  ci: Only log into Conan when uploading packages (5952)
  fix: invariant error in fee-sized `VaultWithdraw` (5876)
  fix: account_tx limit parameter validation for malformed values (5891)
  refactor: Retire fix1543 amendment (5926)
  ci: Only run .exe files during test phase on Windows (5947)
  refactor: Migrate json unit tests to use doctest (5533)
  Change `fixMPTDeliveredAmount` to `Supported::yes` (5833)
  fix: Upload all test binaries (5932)
  chore: Better pre-commit failure message (5940)
This commit is contained in:
Ed Hennis
2025-10-28 18:21:12 -04:00
31 changed files with 2080 additions and 1864 deletions

View File

@@ -138,6 +138,7 @@ test.toplevel > test.csf
test.toplevel > xrpl.json
test.unit_test > xrpl.basics
tests.libxrpl > xrpl.basics
tests.libxrpl > xrpl.json
tests.libxrpl > xrpl.net
xrpl.json > xrpl.basics
xrpl.ledger > xrpl.basics

View File

@@ -50,7 +50,12 @@ on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# When a PR is merged into the develop branch it will be assigned a unique
# group identifier, so execution will continue even if another PR is merged
# while it is still running. In all other cases the group identifier is shared
# per branch, so that any in-progress runs are cancelled when a new commit is
# pushed.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
cancel-in-progress: true
defaults:

View File

@@ -9,7 +9,7 @@ on:
jobs:
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
run-hooks:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@a8d7472b450eb53a1e5228f64552e5974457a21a
uses: XRPLF/actions/.github/workflows/pre-commit.yml@34790936fae4c6c751f62ec8c06696f9c1a5753a
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-a8c7be1" }'

View File

@@ -101,16 +101,32 @@ jobs:
--parallel $(nproc) \
--target "${CMAKE_TARGET}"
- name: Put built binaries in one location
shell: bash
working-directory: ${{ inputs.build_dir }}
env:
BUILD_TYPE_DIR: ${{ runner.os == 'Windows' && inputs.build_type || '' }}
CMAKE_TARGET: ${{ inputs.cmake_target }}
run: |
mkdir -p ./binaries/doctest/
cp ./${BUILD_TYPE_DIR}/rippled* ./binaries/
if [ "${CMAKE_TARGET}" != 'coverage' ]; then
cp ./src/tests/libxrpl/${BUILD_TYPE_DIR}/xrpl.test.* ./binaries/doctest/
fi
- name: Upload rippled artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
env:
BUILD_DIR: ${{ inputs.build_dir }}
with:
name: rippled-${{ inputs.config_name }}
path: ${{ inputs.build_dir }}/${{ runner.os == 'Windows' && inputs.build_type || '' }}/rippled${{ runner.os == 'Windows' && '.exe' || '' }}
path: ${{ env.BUILD_DIR }}/binaries/
retention-days: 3
if-no-files-found: error
- name: Upload coverage report
if: ${{ inputs.cmake_target == 'coverage' }}
if: ${{ github.repository_owner == 'XRPLF' && inputs.cmake_target == 'coverage' }}
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
with:
disable_search: true

View File

@@ -33,6 +33,10 @@ jobs:
container: ${{ inputs.image != '' && inputs.image || null }}
timeout-minutes: 30
steps:
- name: Cleanup workspace
if: ${{ runner.os == 'macOS' }}
uses: XRPLF/actions/.github/actions/cleanup-workspace@3f044c7478548e3c32ff68980eeb36ece02b364e
- name: Download rippled artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
@@ -62,9 +66,24 @@ jobs:
run: |
./rippled --version | grep libvoidstar
- name: Test the binary
- name: Run the embedded tests
if: ${{ inputs.run_tests }}
shell: bash
run: |
./rippled --unittest --unittest-jobs $(nproc)
ctest -j $(nproc) --output-on-failure
- name: Run the separate tests
if: ${{ inputs.run_tests }}
env:
EXT: ${{ runner.os == 'Windows' && '.exe' || '' }}
shell: bash
run: |
for test_file in ./doctest/*${EXT}; do
echo "Executing $test_file"
chmod +x "$test_file"
if [[ "${{ runner.os }}" == "Windows" && "$test_file" == "./doctest/xrpl.test.net.exe" ]]; then
echo "Skipping $test_file on Windows"
else
"$test_file"
fi
done

View File

@@ -84,11 +84,11 @@ jobs:
verbosity: ${{ runner.os == 'Windows' && 'quiet' || 'verbose' }}
- name: Log into Conan remote
if: ${{ github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' }}
if: ${{ github.repository_owner == 'XRPLF' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
run: conan remote login "${CONAN_REMOTE_NAME}" "${{ secrets.CONAN_REMOTE_USERNAME }}" --password "${{ secrets.CONAN_REMOTE_PASSWORD }}"
- name: Upload Conan packages
if: ${{ github.repository_owner == 'XRPLF' && github.event_name != 'pull_request' && github.event_name != 'schedule' }}
if: ${{ github.repository_owner == 'XRPLF' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
env:
FORCE_OPTION: ${{ github.event.inputs.force_upload == 'true' && '--force' || '' }}
run: conan upload "*" --remote="${CONAN_REMOTE_NAME}" --confirm ${FORCE_OPTION}

View File

@@ -7,7 +7,7 @@ function(xrpl_add_test name)
"${CMAKE_CURRENT_SOURCE_DIR}/${name}/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/${name}.cpp"
)
add_executable(${target} EXCLUDE_FROM_ALL ${ARGN} ${sources})
add_executable(${target} ${ARGN} ${sources})
isolate_headers(
${target}

View File

@@ -38,7 +38,7 @@ XRPL_FEATURE(DynamicMPT, Supported::no, VoteBehavior::DefaultNo
XRPL_FIX (TokenEscrowV1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (DelegateV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PriceOracleOrder, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (MPTDeliveredAmount, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMClawbackRounding, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo)
@@ -112,7 +112,6 @@ XRPL_FEATURE(MultiSignReserve, Supported::yes, VoteBehavior::DefaultYe
XRPL_FIX (1578, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(DepositPreauth, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (1623, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (1543, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (1571, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(Checks, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(DepositAuth, Supported::yes, VoteBehavior::DefaultYes)
@@ -156,5 +155,6 @@ XRPL_RETIRE(fix1528)
XRPL_RETIRE(FlowCross)
XRPL_RETIRE(fix1513)
XRPL_RETIRE(fix1515)
XRPL_RETIRE(fix1543)
// clang-format on

View File

@@ -2172,7 +2172,7 @@ class Invariants_test : public beast::unit_test::suite
AccountID account;
int amount;
};
struct Adjustements
struct Adjustments
{
std::optional<int> assetsTotal = {};
std::optional<int> assetsAvailable = {};
@@ -2185,7 +2185,7 @@ class Invariants_test : public beast::unit_test::suite
};
auto constexpr adjust = [&](ApplyView& ac,
ripple::Keylet keylet,
Adjustements args) {
Adjustments args) {
auto sleVault = ac.peek(keylet);
if (!sleVault)
return false;
@@ -2211,9 +2211,11 @@ class Invariants_test : public beast::unit_test::suite
ac.update(sleVault);
if (args.sharesTotal)
{
(*sleShares)[sfOutstandingAmount] =
*(*sleShares)[sfOutstandingAmount] + *args.sharesTotal;
ac.update(sleShares);
ac.update(sleShares);
}
auto const assets = *(*sleVault)[sfAsset];
auto const pseudoId = *(*sleVault)[sfAccount];
@@ -2284,17 +2286,17 @@ class Invariants_test : public beast::unit_test::suite
};
constexpr auto args =
[](AccountID id, int adjustement, auto fn) -> Adjustements {
Adjustements sample = {
.assetsTotal = adjustement,
.assetsAvailable = adjustement,
[](AccountID id, int adjustment, auto fn) -> Adjustments {
Adjustments sample = {
.assetsTotal = adjustment,
.assetsAvailable = adjustment,
.lossUnrealized = 0,
.sharesTotal = adjustement,
.vaultAssets = adjustement,
.sharesTotal = adjustment,
.vaultAssets = adjustment,
.accountAssets = //
AccountAmount{id, -adjustement},
AccountAmount{id, -adjustment},
.accountShares = //
AccountAmount{id, adjustement}};
AccountAmount{id, adjustment}};
fn(sample);
return sample;
};
@@ -2706,7 +2708,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {
args(A2.id(), 0, [&](Adjustments& sample) {
sample.assetsAvailable = (DROPS_PER_XRP * -100).value();
sample.assetsTotal = (DROPS_PER_XRP * -200).value();
sample.sharesTotal = -1;
@@ -2775,7 +2777,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {
args(A2.id(), 0, [&](Adjustments& sample) {
sample.lossUnrealized = 13;
sample.assetsTotal = 20;
}));
@@ -2795,7 +2797,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 100, [&](Adjustements& sample) {
args(A2.id(), 100, [&](Adjustments& sample) {
sample.lossUnrealized = 13;
}));
},
@@ -2816,7 +2818,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {
args(A2.id(), 0, [&](Adjustments& sample) {
sample.assetsMaximum = 1;
}));
},
@@ -2833,7 +2835,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {
args(A2.id(), 0, [&](Adjustments& sample) {
sample.assetsMaximum = -1;
}));
},
@@ -2882,7 +2884,7 @@ class Invariants_test : public beast::unit_test::suite
ac.view().update(sleShares);
return adjust(
ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {}));
ac.view(), keylet, args(A2.id(), 10, [](Adjustments&) {}));
},
XRPAmount{},
STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
@@ -2895,7 +2897,7 @@ class Invariants_test : public beast::unit_test::suite
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const keylet = keylet::vault(A1.id(), ac.view().seq());
adjust(
ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {}));
ac.view(), keylet, args(A2.id(), 10, [](Adjustments&) {}));
auto sleVault = ac.view().peek(keylet);
if (!sleVault)
@@ -3271,7 +3273,9 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {}));
args(A2.id(), 0, [](Adjustments& sample) {
sample.vaultAssets.reset();
}));
},
XRPAmount{},
STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
@@ -3285,7 +3289,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 200, [&](Adjustements& sample) {
args(A2.id(), 200, [&](Adjustments& sample) {
sample.assetsMaximum = 1;
}));
},
@@ -3319,7 +3323,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A3.id(), -10, [&](Adjustements& sample) {
args(A3.id(), -10, [&](Adjustments& sample) {
sample.accountAssets->amount = -100;
}));
},
@@ -3352,7 +3356,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [&](Adjustments& sample) {
sample.vaultAssets = -20;
sample.accountAssets->amount = 10;
}));
@@ -3380,7 +3384,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [&](Adjustments& sample) {
sample.accountAssets->amount = 0;
}));
},
@@ -3399,8 +3403,8 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
sample.accountShares->amount = 0;
args(A2.id(), 10, [&](Adjustments& sample) {
sample.accountShares.reset();
}));
},
XRPAmount{},
@@ -3415,10 +3419,11 @@ class Invariants_test : public beast::unit_test::suite
{"deposit must change vault shares"},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const keylet = keylet::vault(A1.id(), ac.view().seq());
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [](Adjustments& sample) {
sample.sharesTotal = 0;
}));
},
@@ -3440,7 +3445,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [&](Adjustments& sample) {
sample.accountShares->amount = -5;
sample.sharesTotal = -10;
}));
@@ -3453,6 +3458,33 @@ class Invariants_test : public beast::unit_test::suite
precloseXrp,
TxAccount::A2);
doInvariantCheck(
{"deposit and assets outstanding must add up"},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleA3 = ac.view().peek(keylet::account(A3.id()));
(*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
ac.view().update(sleA3);
auto const keylet = keylet::vault(A1.id(), ac.view().seq());
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustments& sample) {
sample.assetsTotal = 11;
}));
},
XRPAmount{2000},
STTx{
ttVAULT_DEPOSIT,
[&](STObject& tx) {
tx[sfAmount] = XRPAmount(10);
tx[sfDelegate] = A3.id();
tx[sfFee] = XRPAmount(2000);
}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp,
TxAccount::A2);
doInvariantCheck(
{"deposit and assets outstanding must add up",
"deposit and assets available must add up"},
@@ -3461,7 +3493,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [&](Adjustments& sample) {
sample.assetsTotal = 7;
sample.assetsAvailable = 7;
}));
@@ -3482,7 +3514,9 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {}));
args(A2.id(), 0, [](Adjustments& sample) {
sample.vaultAssets.reset();
}));
},
XRPAmount{},
STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
@@ -3508,7 +3542,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A3.id(), -10, [&](Adjustements& sample) {
args(A3.id(), -10, [&](Adjustments& sample) {
sample.accountAssets->amount = -100;
}));
},
@@ -3544,7 +3578,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
sample.vaultAssets = 10;
sample.accountAssets->amount = -20;
}));
@@ -3562,7 +3596,7 @@ class Invariants_test : public beast::unit_test::suite
if (!adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
*sample.vaultAssets -= 5;
})))
return false;
@@ -3588,8 +3622,8 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
sample.accountShares->amount = 0;
args(A2.id(), -10, [&](Adjustments& sample) {
sample.accountShares.reset();
}));
},
XRPAmount{},
@@ -3605,7 +3639,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [](Adjustments& sample) {
sample.sharesTotal = 0;
}));
},
@@ -3624,7 +3658,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
sample.accountShares->amount = 5;
sample.sharesTotal = 10;
}));
@@ -3643,7 +3677,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
sample.assetsTotal = -15;
sample.assetsAvailable = -15;
}));
@@ -3654,6 +3688,33 @@ class Invariants_test : public beast::unit_test::suite
precloseXrp,
TxAccount::A2);
doInvariantCheck(
{"withdrawal and assets outstanding must add up"},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleA3 = ac.view().peek(keylet::account(A3.id()));
(*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
ac.view().update(sleA3);
auto const keylet = keylet::vault(A1.id(), ac.view().seq());
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustments& sample) {
sample.assetsTotal = -7;
}));
},
XRPAmount{2000},
STTx{
ttVAULT_WITHDRAW,
[&](STObject& tx) {
tx[sfAmount] = XRPAmount(10);
tx[sfDelegate] = A3.id();
tx[sfFee] = XRPAmount(2000);
}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp,
TxAccount::A2);
auto const precloseMpt =
[&](Account const& A1, Account const& A2, Env& env) -> bool {
env.fund(XRP(1000), A3, A4);
@@ -3713,7 +3774,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
sample.accountShares->amount = 5;
}));
},
@@ -3733,8 +3794,8 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -1, [&](Adjustements& sample) {
sample.vaultAssets = 0;
args(A2.id(), -1, [&](Adjustments& sample) {
sample.vaultAssets.reset();
}));
},
XRPAmount{},
@@ -3752,7 +3813,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {}));
args(A2.id(), 0, [&](Adjustments& sample) {}));
},
XRPAmount{},
STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
@@ -3767,7 +3828,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {}));
args(A2.id(), 0, [&](Adjustments& sample) {}));
},
XRPAmount{},
STTx{
@@ -3785,7 +3846,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A4.id(), 10, [&](Adjustements& sample) {
args(A4.id(), 10, [&](Adjustments& sample) {
sample.sharesTotal = 0;
}));
},
@@ -3806,8 +3867,8 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A4.id(), -10, [&](Adjustements& sample) {
sample.accountShares->amount = 0;
args(A4.id(), -10, [&](Adjustments& sample) {
sample.accountShares.reset();
}));
},
XRPAmount{},
@@ -3829,7 +3890,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A4.id(), -10, [&](Adjustements& sample) {
args(A4.id(), -10, [&](Adjustments& sample) {
sample.accountShares->amount = -8;
sample.assetsTotal = -7;
sample.assetsAvailable = -7;

View File

@@ -19,6 +19,7 @@
#include <test/jtx.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <xrpl/basics/base_uint.h>
@@ -43,6 +44,8 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <optional>
namespace ripple {
class Vault_test : public beast::unit_test::suite
@@ -303,6 +306,55 @@ class Vault_test : public beast::unit_test::suite
BEAST_EXPECT(
env.balance(depositor, shares) == share(200 * scale));
}
else
{
testcase(prefix + " deposit/withdrawal same or less than fee");
auto const amount = env.current()->fees().base;
auto tx = vault.deposit(
{.depositor = depositor,
.id = keylet.key,
.amount = amount});
env(tx);
env.close();
tx = vault.withdraw(
{.depositor = depositor,
.id = keylet.key,
.amount = amount});
env(tx);
env.close();
tx = vault.deposit(
{.depositor = depositor,
.id = keylet.key,
.amount = amount});
env(tx);
env.close();
// Withdraw to 3rd party
tx = vault.withdraw(
{.depositor = depositor,
.id = keylet.key,
.amount = amount});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
tx = vault.deposit(
{.depositor = depositor,
.id = keylet.key,
.amount = amount - 1});
env(tx);
env.close();
tx = vault.withdraw(
{.depositor = depositor,
.id = keylet.key,
.amount = amount - 1});
env(tx);
env.close();
}
{
testcase(
@@ -4830,6 +4882,147 @@ class Vault_test : public beast::unit_test::suite
}
}
void
testDelegate()
{
using namespace test::jtx;
Env env(*this, testable_amendments());
Account alice{"alice"};
Account bob{"bob"};
Account carol{"carol"};
struct CaseArgs
{
PrettyAsset asset = xrpIssue();
};
auto const xrpBalance =
[this](
Env const& env, Account const& account) -> std::optional<long> {
auto sle = env.le(keylet::account(account.id()));
if (BEAST_EXPECT(sle != nullptr))
return sle->getFieldAmount(sfBalance).xrp().drops();
return std::nullopt;
};
auto testCase = [&, this](auto test, CaseArgs args = {}) {
Env env{*this, testable_amendments() | featureSingleAssetVault};
Vault vault{env};
// use different initial amount to distinguish the source balance
env.fund(XRP(10000), alice);
env.fund(XRP(20000), bob);
env.fund(XRP(30000), carol);
env.close();
env(delegate::set(
carol,
alice,
{"Payment",
"VaultCreate",
"VaultSet",
"VaultDelete",
"VaultDeposit",
"VaultWithdraw",
"VaultClawback"}));
test(env, vault, args.asset);
};
testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) {
testcase("delegated vault creation");
auto startBalance = xrpBalance(env, carol);
if (!BEAST_EXPECT(startBalance.has_value()))
return;
auto [tx, keylet] = vault.create({.owner = carol, .asset = asset});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance);
});
testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) {
testcase("delegated deposit and withdrawal");
auto [tx, keylet] = vault.create({.owner = carol, .asset = asset});
env(tx);
env.close();
auto const amount = 1513;
auto const baseFee = env.current()->fees().base;
auto startBalance = xrpBalance(env, carol);
if (!BEAST_EXPECT(startBalance.has_value()))
return;
tx = vault.deposit(
{.depositor = carol,
.id = keylet.key,
.amount = asset(amount)});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - amount);
tx = vault.withdraw(
{.depositor = carol,
.id = keylet.key,
.amount = asset(amount - 1)});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - 1);
tx = vault.withdraw(
{.depositor = carol, .id = keylet.key, .amount = asset(1)});
env(tx);
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - baseFee);
});
testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) {
testcase("delegated withdrawal same as base fee and deletion");
auto [tx, keylet] = vault.create({.owner = carol, .asset = asset});
env(tx);
env.close();
auto const amount = 25537;
auto const baseFee = env.current()->fees().base;
auto startBalance = xrpBalance(env, carol);
if (!BEAST_EXPECT(startBalance.has_value()))
return;
tx = vault.deposit(
{.depositor = carol,
.id = keylet.key,
.amount = asset(amount)});
env(tx);
env.close();
BEAST_EXPECT(
xrpBalance(env, carol) == *startBalance - amount - baseFee);
tx = vault.withdraw(
{.depositor = carol,
.id = keylet.key,
.amount = asset(baseFee)});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - amount);
tx = vault.withdraw(
{.depositor = carol,
.id = keylet.key,
.amount = asset(amount - baseFee)});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - baseFee);
tx = vault.del({.owner = carol, .id = keylet.key});
env(tx, delegate::as(alice));
env.close();
});
}
public:
void
run() override
@@ -4847,6 +5040,7 @@ public:
testFailedPseudoAccount();
testScaleIOU();
testRPC();
testDelegate();
}
};

View File

@@ -1,217 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/json/TestOutputSuite.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/json/Writer.h>
namespace Json {
class JsonWriter_test : public ripple::test::TestOutputSuite
{
public:
void
testTrivial()
{
setup("trivial");
BEAST_EXPECT(output_.empty());
expectResult("");
}
void
testNearTrivial()
{
setup("near trivial");
BEAST_EXPECT(output_.empty());
writer_->output(0);
expectResult("0");
}
void
testPrimitives()
{
setup("true");
writer_->output(true);
expectResult("true");
setup("false");
writer_->output(false);
expectResult("false");
setup("23");
writer_->output(23);
expectResult("23");
setup("23.0");
writer_->output(23.0);
expectResult("23.0");
setup("23.5");
writer_->output(23.5);
expectResult("23.5");
setup("a string");
writer_->output("a string");
expectResult("\"a string\"");
setup("nullptr");
writer_->output(nullptr);
expectResult("null");
}
void
testEmpty()
{
setup("empty array");
writer_->startRoot(Writer::array);
writer_->finish();
expectResult("[]");
setup("empty object");
writer_->startRoot(Writer::object);
writer_->finish();
expectResult("{}");
}
void
testEscaping()
{
setup("backslash");
writer_->output("\\");
expectResult("\"\\\\\"");
setup("quote");
writer_->output("\"");
expectResult("\"\\\"\"");
setup("backslash and quote");
writer_->output("\\\"");
expectResult("\"\\\\\\\"\"");
setup("escape embedded");
writer_->output("this contains a \\ in the middle of it.");
expectResult("\"this contains a \\\\ in the middle of it.\"");
setup("remaining escapes");
writer_->output("\b\f\n\r\t");
expectResult("\"\\b\\f\\n\\r\\t\"");
}
void
testArray()
{
setup("empty array");
writer_->startRoot(Writer::array);
writer_->append(12);
writer_->finish();
expectResult("[12]");
}
void
testLongArray()
{
setup("long array");
writer_->startRoot(Writer::array);
writer_->append(12);
writer_->append(true);
writer_->append("hello");
writer_->finish();
expectResult("[12,true,\"hello\"]");
}
void
testEmbeddedArraySimple()
{
setup("embedded array simple");
writer_->startRoot(Writer::array);
writer_->startAppend(Writer::array);
writer_->finish();
writer_->finish();
expectResult("[[]]");
}
void
testObject()
{
setup("object");
writer_->startRoot(Writer::object);
writer_->set("hello", "world");
writer_->finish();
expectResult("{\"hello\":\"world\"}");
}
void
testComplexObject()
{
setup("complex object");
writer_->startRoot(Writer::object);
writer_->set("hello", "world");
writer_->startSet(Writer::array, "array");
writer_->append(true);
writer_->append(12);
writer_->startAppend(Writer::array);
writer_->startAppend(Writer::object);
writer_->set("goodbye", "cruel world.");
writer_->startSet(Writer::array, "subarray");
writer_->append(23.5);
writer_->finishAll();
expectResult(
"{\"hello\":\"world\",\"array\":[true,12,"
"[{\"goodbye\":\"cruel world.\","
"\"subarray\":[23.5]}]]}");
}
void
testJson()
{
setup("object");
Json::Value value(Json::objectValue);
value["foo"] = 23;
writer_->startRoot(Writer::object);
writer_->set("hello", value);
writer_->finish();
expectResult("{\"hello\":{\"foo\":23}}");
}
void
run() override
{
testTrivial();
testNearTrivial();
testPrimitives();
testEmpty();
testEscaping();
testArray();
testLongArray();
testEmbeddedArraySimple();
testObject();
testComplexObject();
testJson();
}
};
BEAST_DEFINE_TESTSUITE(JsonWriter, json, ripple);
} // namespace Json

File diff suppressed because it is too large Load Diff

View File

@@ -190,12 +190,6 @@ public:
}
{
// now make a limit (= 0) query for the same data
// since we operate on the admin port, the limit
// value of 0 is not adjusted into tuned ranges for admin requests
// so we literally get 0 elements in that case. For non-admin
// requests, we get limit defaults applied thus all our results
// come back (we are below the min results limit)
Json::Value jvParams;
jvParams[jss::account] = bob.human();
jvParams[jss::limit] = 0u;
@@ -203,18 +197,7 @@ public:
"json",
"account_offers",
jvParams.toStyledString())[jss::result];
auto const& jro = jrr[jss::offers];
if (asAdmin)
{
// limit == 0 is invalid
BEAST_EXPECT(jrr.isMember(jss::error_message));
}
else
{
// Call should enforce min limit of 10
BEAST_EXPECT(checkArraySize(jro, 3u));
BEAST_EXPECT(!jrr.isMember(jss::marker));
}
BEAST_EXPECT(jrr.isMember(jss::error_message));
}
}

View File

@@ -193,26 +193,26 @@ class AccountTx_test : public beast::unit_test::suite
j[jss::result][jss::error] == RPC::get_error_info(code).token;
};
Json::Value jParms;
jParms[jss::api_version] = apiVersion;
Json::Value jParams;
jParams[jss::api_version] = apiVersion;
BEAST_EXPECT(isErr(
env.rpc("json", "account_tx", to_string(jParms)),
env.rpc("json", "account_tx", to_string(jParams)),
rpcINVALID_PARAMS));
jParms[jss::account] = "0xDEADBEEF";
jParams[jss::account] = "0xDEADBEEF";
BEAST_EXPECT(isErr(
env.rpc("json", "account_tx", to_string(jParms)),
env.rpc("json", "account_tx", to_string(jParams)),
rpcACT_MALFORMED));
jParms[jss::account] = A1.human();
jParams[jss::account] = A1.human();
BEAST_EXPECT(hasTxs(
env.rpc(apiVersion, "json", "account_tx", to_string(jParms))));
env.rpc(apiVersion, "json", "account_tx", to_string(jParams))));
// Ledger min/max index
{
Json::Value p{jParms};
Json::Value p{jParams};
p[jss::ledger_index_min] = -1;
p[jss::ledger_index_max] = -1;
BEAST_EXPECT(hasTxs(
@@ -247,7 +247,7 @@ class AccountTx_test : public beast::unit_test::suite
}
// Ledger index min only
{
Json::Value p{jParms};
Json::Value p{jParams};
p[jss::ledger_index_min] = -1;
BEAST_EXPECT(hasTxs(
env.rpc(apiVersion, "json", "account_tx", to_string(p))));
@@ -270,7 +270,7 @@ class AccountTx_test : public beast::unit_test::suite
// Ledger index max only
{
Json::Value p{jParms};
Json::Value p{jParams};
p[jss::ledger_index_max] = -1;
BEAST_EXPECT(hasTxs(
env.rpc(apiVersion, "json", "account_tx", to_string(p))));
@@ -298,7 +298,7 @@ class AccountTx_test : public beast::unit_test::suite
// Ledger Sequence
{
Json::Value p{jParms};
Json::Value p{jParams};
p[jss::ledger_index] = env.closed()->info().seq;
BEAST_EXPECT(hasTxs(
@@ -319,7 +319,7 @@ class AccountTx_test : public beast::unit_test::suite
// Ledger Hash
{
Json::Value p{jParms};
Json::Value p{jParams};
p[jss::ledger_hash] = to_string(env.closed()->info().hash);
BEAST_EXPECT(hasTxs(
@@ -332,9 +332,9 @@ class AccountTx_test : public beast::unit_test::suite
// Ledger index max/min/index all specified
// ERRORS out with invalid Parenthesis
{
jParms[jss::account] = "0xDEADBEEF";
jParms[jss::account] = A1.human();
Json::Value p{jParms};
jParams[jss::account] = "0xDEADBEEF";
jParams[jss::account] = A1.human();
Json::Value p{jParams};
p[jss::ledger_index_max] = -1;
p[jss::ledger_index_min] = -1;
@@ -351,7 +351,7 @@ class AccountTx_test : public beast::unit_test::suite
// Ledger index max only
{
Json::Value p{jParms};
Json::Value p{jParams};
p[jss::ledger_index_max] = env.current()->info().seq;
if (apiVersion < 2u)
BEAST_EXPECT(hasTxs(
@@ -382,7 +382,7 @@ class AccountTx_test : public beast::unit_test::suite
}
// test binary and forward for bool/non bool values
{
Json::Value p{jParms};
Json::Value p{jParams};
p[jss::binary] = "asdf";
if (apiVersion < 2u)
{
@@ -410,6 +410,117 @@ class AccountTx_test : public beast::unit_test::suite
result = env.rpc("json", "account_tx", to_string(p));
BEAST_EXPECT(result[jss::result][jss::status] == "success");
}
// test limit with malformed values
{
Json::Value p{jParams};
// Test case: limit = 0 should fail (below minimum)
p[jss::limit] = 0;
BEAST_EXPECT(isErr(
env.rpc("json", "account_tx", to_string(p)),
rpcINVALID_PARAMS));
// Test case: limit = 1.2 should fail (not an integer)
p[jss::limit] = 1.2;
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = "10" should fail (string instead of integer)
p[jss::limit] = "10";
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = true should fail (boolean instead of integer)
p[jss::limit] = true;
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = false should fail (boolean instead of integer)
p[jss::limit] = false;
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = -1 should fail (negative number)
p[jss::limit] = -1;
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = [] should fail (array instead of integer)
p[jss::limit] = Json::Value(Json::arrayValue);
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = {} should fail (object instead of integer)
p[jss::limit] = Json::Value(Json::objectValue);
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = "malformed" should fail (malformed string)
p[jss::limit] = "malformed";
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = ["limit"] should fail (array with string)
p[jss::limit] = Json::Value(Json::arrayValue);
p[jss::limit].append("limit");
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = {"limit": 10} should fail (object with
// property)
p[jss::limit] = Json::Value(Json::objectValue);
p[jss::limit][jss::limit] = 10;
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::error_message] ==
RPC::expected_field_message(jss::limit, "unsigned integer"));
// Test case: limit = 10 should succeed (valid integer)
p[jss::limit] = 10;
BEAST_EXPECT(
env.rpc(
"json",
"account_tx",
to_string(p))[jss::result][jss::status] == "success");
}
}
void

View File

@@ -1633,6 +1633,20 @@ public:
"Invalid field 'limit', not unsigned integer.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
jvParams[jss::taker] = env.master.human();
jvParams[jss::limit] = 0; // must be > 0
jvParams[jss::taker_pays][jss::currency] = "XRP";
jvParams[jss::taker_gets][jss::currency] = "USD";
jvParams[jss::taker_gets][jss::issuer] = gw.human();
auto const jrr = env.rpc(
"json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::error] == "invalidParams");
BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit'.");
}
{
Json::Value jvParams;
jvParams[jss::ledger_index] = "validated";
@@ -1710,11 +1724,6 @@ public:
BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? 1u : 0u));
// NOTE - a marker field is not returned for this method
jvParams[jss::limit] = 0u;
jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::offers].isArray());
BEAST_EXPECT(jrr[jss::offers].size() == 0u);
jvParams[jss::limit] = RPC::Tuning::bookOffers.rmax + 1;
jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result];
BEAST_EXPECT(jrr[jss::offers].isArray());

View File

@@ -12,5 +12,7 @@ xrpl_add_test(basics)
target_link_libraries(xrpl.test.basics PRIVATE xrpl.imports.test)
xrpl_add_test(crypto)
target_link_libraries(xrpl.test.crypto PRIVATE xrpl.imports.test)
xrpl_add_test(json)
target_link_libraries(xrpl.test.json PRIVATE xrpl.imports.test)
xrpl_add_test(net)
target_link_libraries(xrpl.test.net PRIVATE xrpl.imports.test)

View File

@@ -17,50 +17,43 @@
*/
//==============================================================================
#include <test/json/TestOutputSuite.h>
#include <xrpl/json/Output.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_writer.h>
namespace Json {
#include <doctest/doctest.h>
struct Output_test : ripple::test::TestOutputSuite
#include <string>
using namespace ripple;
using namespace Json;
TEST_SUITE_BEGIN("JsonOutput");
static void
checkOutput(std::string const& valueDesc)
{
void
runTest(std::string const& name, std::string const& valueDesc)
{
setup(name);
Json::Value value;
BEAST_EXPECT(Json::Reader().parse(valueDesc, value));
auto out = stringOutput(output_);
outputJson(value, out);
std::string output;
Json::Value value;
REQUIRE(Json::Reader().parse(valueDesc, value));
auto out = stringOutput(output);
outputJson(value, out);
// Compare with the original version.
auto expected = Json::FastWriter().write(value);
expectResult(expected);
expectResult(valueDesc);
expectResult(jsonAsString(value));
}
auto expected = Json::FastWriter().write(value);
CHECK(output == expected);
CHECK(output == valueDesc);
CHECK(output == jsonAsString(value));
}
void
runTest(std::string const& name)
{
runTest(name, name);
}
TEST_CASE("output cases")
{
checkOutput("{}");
checkOutput("[]");
checkOutput(R"([23,4.25,true,null,"string"])");
checkOutput(R"({"hello":"world"})");
checkOutput("[{}]");
checkOutput("[[]]");
checkOutput(R"({"array":[{"12":23},{},null,false,0.5]})");
}
void
run() override
{
runTest("empty dict", "{}");
runTest("empty array", "[]");
runTest("array", "[23,4.25,true,null,\"string\"]");
runTest("dict", "{\"hello\":\"world\"}");
runTest("array dict", "[{}]");
runTest("array array", "[[]]");
runTest("more complex", "{\"array\":[{\"12\":23},{},null,false,0.5]}");
}
};
BEAST_DEFINE_TESTSUITE(Output, json, ripple);
} // namespace Json
TEST_SUITE_END();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,192 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/json/Writer.h>
#include <doctest/doctest.h>
#include <google/protobuf/stubs/port.h>
#include <memory>
#include <string>
using namespace ripple;
using namespace Json;
TEST_SUITE_BEGIN("JsonWriter");
struct WriterFixture
{
std::string output;
std::unique_ptr<Writer> writer;
WriterFixture()
{
writer = std::make_unique<Writer>(stringOutput(output));
}
void
reset()
{
output.clear();
writer = std::make_unique<Writer>(stringOutput(output));
}
void
expectOutput(std::string const& expected) const
{
CHECK(output == expected);
}
void
checkOutputAndReset(std::string const& expected)
{
expectOutput(expected);
reset();
}
};
TEST_CASE_FIXTURE(WriterFixture, "trivial")
{
CHECK(output.empty());
checkOutputAndReset("");
}
TEST_CASE_FIXTURE(WriterFixture, "near trivial")
{
CHECK(output.empty());
writer->output(0);
checkOutputAndReset("0");
}
TEST_CASE_FIXTURE(WriterFixture, "primitives")
{
writer->output(true);
checkOutputAndReset("true");
writer->output(false);
checkOutputAndReset("false");
writer->output(23);
checkOutputAndReset("23");
writer->output(23.0);
checkOutputAndReset("23.0");
writer->output(23.5);
checkOutputAndReset("23.5");
writer->output("a string");
checkOutputAndReset(R"("a string")");
writer->output(nullptr);
checkOutputAndReset("null");
}
TEST_CASE_FIXTURE(WriterFixture, "empty")
{
writer->startRoot(Writer::array);
writer->finish();
checkOutputAndReset("[]");
writer->startRoot(Writer::object);
writer->finish();
checkOutputAndReset("{}");
}
TEST_CASE_FIXTURE(WriterFixture, "escaping")
{
writer->output("\\");
checkOutputAndReset(R"("\\")");
writer->output("\"");
checkOutputAndReset(R"("\"")");
writer->output("\\\"");
checkOutputAndReset(R"("\\\"")");
writer->output("this contains a \\ in the middle of it.");
checkOutputAndReset(R"("this contains a \\ in the middle of it.")");
writer->output("\b\f\n\r\t");
checkOutputAndReset(R"("\b\f\n\r\t")");
}
TEST_CASE_FIXTURE(WriterFixture, "array")
{
writer->startRoot(Writer::array);
writer->append(12);
writer->finish();
checkOutputAndReset("[12]");
}
TEST_CASE_FIXTURE(WriterFixture, "long array")
{
writer->startRoot(Writer::array);
writer->append(12);
writer->append(true);
writer->append("hello");
writer->finish();
checkOutputAndReset(R"([12,true,"hello"])");
}
TEST_CASE_FIXTURE(WriterFixture, "embedded array simple")
{
writer->startRoot(Writer::array);
writer->startAppend(Writer::array);
writer->finish();
writer->finish();
checkOutputAndReset("[[]]");
}
TEST_CASE_FIXTURE(WriterFixture, "object")
{
writer->startRoot(Writer::object);
writer->set("hello", "world");
writer->finish();
checkOutputAndReset(R"({"hello":"world"})");
}
TEST_CASE_FIXTURE(WriterFixture, "complex object")
{
writer->startRoot(Writer::object);
writer->set("hello", "world");
writer->startSet(Writer::array, "array");
writer->append(true);
writer->append(12);
writer->startAppend(Writer::array);
writer->startAppend(Writer::object);
writer->set("goodbye", "cruel world.");
writer->startSet(Writer::array, "subarray");
writer->append(23.5);
writer->finishAll();
checkOutputAndReset(
R"({"hello":"world","array":[true,12,[{"goodbye":"cruel world.","subarray":[23.5]}]]})");
}
TEST_CASE_FIXTURE(WriterFixture, "json value")
{
Json::Value value(Json::objectValue);
value["foo"] = 23;
writer->startRoot(Writer::object);
writer->set("hello", value);
writer->finish();
checkOutputAndReset(R"({"hello":{"foo":23}})");
}
TEST_SUITE_END();

View File

@@ -0,0 +1,2 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>

View File

@@ -118,13 +118,6 @@ escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
return tesSUCCESS;
}
std::uint32_t
EscrowCreate::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0;
}
NotTEC
EscrowCreate::preflight(PreflightContext const& ctx)
{
@@ -639,13 +632,6 @@ EscrowFinish::checkExtraFeatures(PreflightContext const& ctx)
ctx.rules.enabled(featureCredentials);
}
std::uint32_t
EscrowFinish::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0;
}
NotTEC
EscrowFinish::preflight(PreflightContext const& ctx)
{
@@ -1225,13 +1211,6 @@ EscrowFinish::doApply()
//------------------------------------------------------------------------------
std::uint32_t
EscrowCancel::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0;
}
NotTEC
EscrowCancel::preflight(PreflightContext const& ctx)
{

View File

@@ -36,9 +36,6 @@ public:
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
@@ -63,9 +60,6 @@ public:
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
@@ -93,9 +87,6 @@ public:
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -2582,13 +2582,12 @@ ValidVault::visitEntry(
after != nullptr && (before != nullptr || !isDelete),
"ripple::ValidVault::visitEntry : some object is available");
// `Number balance` will capture the difference (delta) between "before"
// Number balanceDelta will capture the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), so the
// invariants can validate that the change in account balances matches the
// change in vault balances, stored to deltas_ at the end of this function.
Number balance{};
Number balanceDelta{};
// By default do not add anything to deltas
std::int8_t sign = 0;
if (before)
{
@@ -2601,18 +2600,18 @@ ValidVault::visitEntry(
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
beforeMPTs_.push_back(Shares::make(*before));
balance = static_cast<std::int64_t>(
balanceDelta = static_cast<std::int64_t>(
before->getFieldU64(sfOutstandingAmount));
sign = 1;
break;
case ltMPTOKEN:
balance =
balanceDelta =
static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
sign = -1;
break;
case ltACCOUNT_ROOT:
case ltRIPPLE_STATE:
balance = before->getFieldAmount(sfBalance);
balanceDelta = before->getFieldAmount(sfBalance);
sign = -1;
break;
default:;
@@ -2630,18 +2629,18 @@ ValidVault::visitEntry(
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
afterMPTs_.push_back(Shares::make(*after));
balance -= Number(static_cast<std::int64_t>(
balanceDelta -= Number(static_cast<std::int64_t>(
after->getFieldU64(sfOutstandingAmount)));
sign = 1;
break;
case ltMPTOKEN:
balance -= Number(
balanceDelta -= Number(
static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
sign = -1;
break;
case ltACCOUNT_ROOT:
case ltRIPPLE_STATE:
balance -= Number(after->getFieldAmount(sfBalance));
balanceDelta -= Number(after->getFieldAmount(sfBalance));
sign = -1;
break;
default:;
@@ -2649,8 +2648,13 @@ ValidVault::visitEntry(
}
uint256 const key = (before ? before->key() : after->key());
if (sign && balance != zero)
deltas_[key] = balance * sign;
// Append to deltas if sign is non-zero, i.e. an object of an interesting
// type has been updated. A transaction may update an object even when
// its balance has not changed, e.g. transaction fee equals the amount
// transferred to the account. We intentionally do not compare balanceDelta
// against zero, to avoid missing such updates.
if (sign != 0)
deltas_[key] = balanceDelta * sign;
}
bool
@@ -2958,6 +2962,23 @@ ValidVault::finalize(
},
vaultAsset.value());
};
auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
auto ret = deltaAssets(tx[sfAccount]);
// Nothing returned or not XRP transaction
if (!ret.has_value() || !vaultAsset.native())
return ret;
// Delegated transaction; no need to compensate for fees
if (auto const delegate = tx[~sfDelegate];
delegate.has_value() && *delegate != tx[sfAccount])
return ret;
*ret += fee.drops();
if (*ret == zero)
return std::nullopt;
return ret;
};
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
auto const it = [&]() {
if (id == afterVault.pseudoId)
@@ -3128,20 +3149,7 @@ ValidVault::finalize(
if (!issuerDeposit)
{
auto const accountDeltaAssets =
[&]() -> std::optional<Number> {
if (auto ret = deltaAssets(tx[sfAccount]); ret)
{
// Compensate for transaction fee deduced from
// sfAccount
if (vaultAsset.native())
*ret += fee.drops();
if (*ret != zero)
return ret;
}
return std::nullopt;
}();
auto const accountDeltaAssets = deltaAssetsTxAccount();
if (!accountDeltaAssets)
{
JLOG(j.fatal()) << //
@@ -3194,7 +3202,7 @@ ValidVault::finalize(
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares)
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault shares";
@@ -3263,20 +3271,7 @@ ValidVault::finalize(
if (!issuerWithdrawal)
{
auto const accountDeltaAssets =
[&]() -> std::optional<Number> {
if (auto ret = deltaAssets(tx[sfAccount]); ret)
{
// Compensate for transaction fee deduced from
// sfAccount
if (vaultAsset.native())
*ret += fee.drops();
if (*ret != zero)
return ret;
}
return std::nullopt;
}();
auto const accountDeltaAssets = deltaAssetsTxAccount();
auto const otherAccountDelta =
[&]() -> std::optional<Number> {
if (auto const destination = tx[~sfDestination];
@@ -3333,7 +3328,7 @@ ValidVault::finalize(
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares)
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault shares";
@@ -3418,7 +3413,7 @@ ValidVault::finalize(
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares)
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change vault shares";

View File

@@ -175,13 +175,6 @@ PayChanCreate::makeTxConsequences(PreflightContext const& ctx)
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
std::uint32_t
PayChanCreate::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0;
}
NotTEC
PayChanCreate::preflight(PreflightContext const& ctx)
{
@@ -335,13 +328,6 @@ PayChanFund::makeTxConsequences(PreflightContext const& ctx)
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
}
std::uint32_t
PayChanFund::getFlagsMask(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0;
}
NotTEC
PayChanFund::preflight(PreflightContext const& ctx)
{
@@ -434,10 +420,9 @@ PayChanClaim::checkExtraFeatures(PreflightContext const& ctx)
}
std::uint32_t
PayChanClaim::getFlagsMask(PreflightContext const& ctx)
PayChanClaim::getFlagsMask(PreflightContext const&)
{
// 0 means "Allow any flags"
return ctx.rules.enabled(fix1543) ? tfPayChanClaimMask : 0;
return tfPayChanClaimMask;
}
NotTEC

View File

@@ -36,9 +36,6 @@ public:
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
@@ -65,9 +62,6 @@ public:
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -704,15 +704,21 @@ readLimitField(
JsonContext const& context)
{
limit = range.rdefault;
if (auto const& jvLimit = context.params[jss::limit])
{
if (!(jvLimit.isUInt() || (jvLimit.isInt() && jvLimit.asInt() >= 0)))
return RPC::expected_field_error(jss::limit, "unsigned integer");
if (!context.params.isMember(jss::limit) ||
context.params[jss::limit].isNull())
return std::nullopt;
auto const& jvLimit = context.params[jss::limit];
if (!(jvLimit.isUInt() || (jvLimit.isInt() && jvLimit.asInt() >= 0)))
return RPC::expected_field_error(jss::limit, "unsigned integer");
limit = jvLimit.asUInt();
if (limit == 0)
return RPC::invalid_field_error(jss::limit);
if (!isUnlimited(context.role))
limit = std::max(range.rmin, std::min(range.rmax, limit));
limit = jvLimit.asUInt();
if (!isUnlimited(context.role))
limit = std::max(range.rmin, std::min(range.rmax, limit));
}
return std::nullopt;
}

View File

@@ -45,6 +45,9 @@ static LimitRange constexpr accountObjects = {10, 200, 400};
/** Limits for the account_offers command. */
static LimitRange constexpr accountOffers = {10, 200, 400};
/** Limits for the account_tx command. */
static LimitRange constexpr accountTx = {10, 200, 400};
/** Limits for the book_offers command. */
static LimitRange constexpr bookOffers = {0, 60, 100};

View File

@@ -103,9 +103,6 @@ doAccountChannels(RPC::JsonContext& context)
if (auto err = readLimitField(limit, RPC::Tuning::accountChannels, context))
return *err;
if (limit == 0u)
return rpcError(rpcINVALID_PARAMS);
Json::Value jsonChannels{Json::arrayValue};
struct VisitData
{

View File

@@ -120,9 +120,6 @@ doAccountLines(RPC::JsonContext& context)
if (auto err = readLimitField(limit, RPC::Tuning::accountLines, context))
return *err;
if (limit == 0)
return rpcError(rpcINVALID_PARAMS);
// this flag allows the requester to ask incoming trustlines in default
// state be omitted
bool ignoreDefault = params.isMember(jss::ignore_default) &&

View File

@@ -86,9 +86,6 @@ doAccountOffers(RPC::JsonContext& context)
if (auto err = readLimitField(limit, RPC::Tuning::accountOffers, context))
return *err;
if (limit == 0)
return RPC::invalid_field_error(jss::limit);
Json::Value& jsonOffers(result[jss::offers] = Json::arrayValue);
std::vector<std::shared_ptr<SLE const>> offers;
uint256 startAfter = beast::zero;

View File

@@ -26,6 +26,8 @@
#include <xrpld/rpc/DeliveredAmount.h>
#include <xrpld/rpc/MPTokenIssuanceID.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
@@ -429,7 +431,10 @@ doAccountTxJson(RPC::JsonContext& context)
return RPC::invalid_field_error(jss::forward);
}
args.limit = params.isMember(jss::limit) ? params[jss::limit].asUInt() : 0;
if (auto const err =
RPC::readLimitField(args.limit, RPC::Tuning::accountTx, context))
return *err;
args.binary = params.isMember(jss::binary) && params[jss::binary].asBool();
args.forward =
params.isMember(jss::forward) && params[jss::forward].asBool();