Compare commits

..

30 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
dcce21b835 Fix date/ctid missing from result level in API v3, fix pre-commit errors
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-04-03 10:52:42 -04:00
copilot-swe-agent[bot]
7856198e4e Fix: remove non-canonical fields from tx_json in API v3
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-04-03 10:52:42 -04:00
Mayukha Vadari
81555d5456 refactor: Reorganize RPC handler files (#6628) 2026-04-02 23:46:17 +00:00
Ayaz Salikhov
6b55c4cdc8 chore: Update XRPLF/actions (#6713) 2026-04-02 21:34:20 +00:00
yinyiqian1
3414a1776b docs: Add explanatory comment to checkFee (#6631) 2026-04-02 20:48:35 +00:00
yinyiqian1
6d9ed125f3 fix: Decouple reserve from fee in delegate payment (#6568) 2026-04-02 20:48:00 +00:00
Vito Tumas
02fa55df8d fix: Check trustline limits for share-denominated vault withdrawals (#6645) 2026-04-01 19:31:45 +00:00
Valentin Balaschenko
6e2452207d fix: Remove fatal assertion on Linux thread name truncation (#6690) 2026-04-01 16:56:45 +00:00
Alex Kremer
29e49abd3c chore: Enable clang-tidy coreguidelines checks (#6698)
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2026-04-01 15:46:14 +00:00
Ayaz Salikhov
ae21f53e4d ci: Allow uploading artifacts for XRPLF org (#6702) 2026-04-01 13:37:35 +00:00
Vito Tumas
bee1056faa fix: Enforce aggregate MaximumAmount in multi-send MPT (#6644)
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-04-01 13:35:13 +00:00
Ayaz Salikhov
b6aa4a8fde chore: Use nudb recipe from the upstream (#6701) 2026-04-01 10:33:02 +00:00
Mayukha Vadari
a9afd2c116 fix: Fix previous ledger size typo in RCLConsensus (#6696) 2026-03-31 19:56:30 +00:00
Alex Kremer
2502befb42 chore: Enable clang-tidy misc checks (#6655) 2026-03-31 17:29:45 +00:00
Ayaz Salikhov
c3fae847f3 ci: Use pull_request_target to check for signed commits (#6697) 2026-03-31 17:14:41 +00:00
Bart
7f53351920 chore: Remove unnecessary clang-format off/on directives (#6682)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-03-31 15:38:04 +00:00
Pratik Mankawde
bb95a7d6cd fix: Fix Workers::stop() race between m_allPaused and m_runningTaskCount (#6574)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 15:06:04 +00:00
Ayaz Salikhov
5c8dfe5456 ci: Only publish docs in public repos (#6687) 2026-03-30 17:15:40 +00:00
Alex Kremer
ab8c168e3b chore: Enable remaining clang-tidy performance checks (#6648)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-30 17:08:47 +00:00
Jingchen
3a477e4d01 refactor: Address PR comments after the modularisation PRs (#6389)
Signed-off-by: JCW <a1q123456@users.noreply.github.com>
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-03-30 15:22:38 +00:00
Alex Kremer
96bfc32fe2 chore: Fix clang-tidy header filter (#6686) 2026-03-30 14:59:53 +00:00
dependabot[bot]
de671863e2 ci: [DEPENDABOT] bump actions/deploy-pages from 4.0.5 to 5.0.0 (#6684)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 14:09:57 +00:00
dependabot[bot]
e0cabb9f8c ci: [DEPENDABOT] bump codecov/codecov-action from 5.5.3 to 6.0.0 (#6685)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 13:57:32 +00:00
Pratik Mankawde
3d9c545f59 fix: Guard Coro::resume() against completed coroutines (#6608)
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 18:52:18 +00:00
Vito Tumas
9b944ee8c2 refactor: Split LoanInvariant into LoanBrokerInvariant and LoanInvariant (#6674) 2026-03-27 18:35:42 +00:00
Ayaz Salikhov
509677abfd ci: Don't publish docs on release branches (#6673) 2026-03-26 14:11:37 +00:00
Jingchen
addc1e8e25 refactor: Make function naming in ServiceRegistry consistent (#6390)
Signed-off-by: JCW <a1q123456@users.noreply.github.com>
Co-authored-by: Ed Hennis <ed@ripple.com>
2026-03-26 14:11:16 +00:00
Valentin Balaschenko
faf69da4b0 chore: Shorten job names to stay within Linux 15-char thread limit (#6669) 2026-03-26 14:10:51 +00:00
Vito Tumas
76e3b4fb0f fix: Improve loan invariant message (#6668) 2026-03-26 12:40:26 +00:00
Ayaz Salikhov
e8bdbf975a ci: Upload artifacts only in public repositories (#6670) 2026-03-26 12:37:37 +00:00
100 changed files with 858 additions and 409 deletions

View File

@@ -76,11 +76,11 @@ fi
if ! grep -q 'Dev Null' src/test/rpc/ValidatorInfo_test.cpp; then
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/rpc/ValidatorInfo_test.cpp)" > src/test/rpc/ValidatorInfo_test.cpp
fi
if ! grep -q 'Dev Null' src/xrpld/rpc/handlers/DoManifest.cpp; then
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/DoManifest.cpp)" > src/xrpld/rpc/handlers/DoManifest.cpp
if ! grep -q 'Dev Null' src/xrpld/rpc/handlers/server_info/Manifest.cpp; then
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/server_info/Manifest.cpp)" > src/xrpld/rpc/handlers/server_info/Manifest.cpp
fi
if ! grep -q 'Dev Null' src/xrpld/rpc/handlers/ValidatorInfo.cpp; then
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/ValidatorInfo.cpp)" > src/xrpld/rpc/handlers/ValidatorInfo.cpp
if ! grep -q 'Dev Null' src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp; then
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp)" > src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp
fi
if ! grep -q 'Bougalis' include/xrpl/basics/SlabAllocator.h; then
echo -e "// Copyright (c) 2022, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/SlabAllocator.h)" > include/xrpl/basics/SlabAllocator.h # cspell: ignore Nikolaos Bougalis nikb

View File

@@ -10,4 +10,4 @@ permissions:
jobs:
check_commits:
uses: XRPLF/actions/.github/workflows/check-pr-commits.yml@481048b78b94ac3343d1292b4ef125a813879f2b
uses: XRPLF/actions/.github/workflows/check-pr-commits.yml@e2c7f400d1e85ae65dad552fd425169fbacca4a3

View File

@@ -11,4 +11,4 @@ on:
jobs:
check_title:
if: ${{ github.event.pull_request.draft != true }}
uses: XRPLF/actions/.github/workflows/check-pr-title.yml@e2c7f400d1e85ae65dad552fd425169fbacca4a3
uses: XRPLF/actions/.github/workflows/check-pr-title.yml@a5d8dd35be543365e90a11358447130c8763871d

View File

@@ -14,7 +14,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@e7896f15cc60d0da1a272c77ee5c4026b424f9c7
uses: XRPLF/actions/.github/workflows/pre-commit.yml@9307df762265e15c745ddcdb38a581c989f7f349
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'

View File

@@ -47,7 +47,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@2bbc2dc1abeec7bfaa886804ab86871ac201764e
uses: XRPLF/actions/prepare-runner@90f11ee655d1687824fb8793db770477d52afbab
with:
enable_ccache: false

View File

@@ -107,7 +107,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@2bbc2dc1abeec7bfaa886804ab86871ac201764e
uses: XRPLF/actions/prepare-runner@90f11ee655d1687824fb8793db770477d52afbab
with:
enable_ccache: ${{ inputs.ccache_enabled }}

View File

@@ -35,7 +35,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@2bbc2dc1abeec7bfaa886804ab86871ac201764e
uses: XRPLF/actions/prepare-runner@90f11ee655d1687824fb8793db770477d52afbab
with:
enable_ccache: false

View File

@@ -70,7 +70,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@2bbc2dc1abeec7bfaa886804ab86871ac201764e
uses: XRPLF/actions/prepare-runner@90f11ee655d1687824fb8793db770477d52afbab
with:
enable_ccache: false

View File

@@ -6,6 +6,13 @@ For info about how [API versioning](https://xrpl.org/request-formatting.html#api
## Breaking Changes
### Modifications to `tx` and `account_tx`
In API version 2, the `tx_json` field in `tx` and `account_tx` responses includes server-added lower-case fields (`date`, `ledger_index`, and `ctid`) that are not part of the canonical signed transaction. In API version 3, these fields are removed from `tx_json` and are only present at the top-level result object.
- **Before (API v2)**: The `tx_json` object in the response contained `date`, `ledger_index`, and `ctid` fields alongside the canonical PascalCase transaction fields.
- **After (API v3)**: The `tx_json` object contains only the canonical signed transaction fields. The `date`, `ledger_index`, and `ctid` fields appear exclusively at the top-level result object.
### Modifications to `amm_info`
The order of error checks has been changed to provide more specific error messages. ([#4924](https://github.com/XRPLF/rippled/pull/4924))

View File

@@ -23,9 +23,10 @@ struct JsonOptions
none = 0b0000'0000,
include_date = 0b0000'0001,
disable_API_prior_V2 = 0b0000'0010,
disable_API_prior_V3 = 0b0000'0100,
// IMPORTANT `_all` must be union of all of the above; see also operator~
_all = 0b0000'0011
_all = 0b0000'0111
// clang-format on
};

View File

@@ -23,7 +23,7 @@ namespace {
// and follow the format described at http://semver.org/
//------------------------------------------------------------------------------
// clang-format off
char const* const versionString = "3.2.0-b3"
char const* const versionString = "3.2.0-b0"
// clang-format on
;

View File

@@ -363,6 +363,13 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
auto const balance = (*sle)[sfBalance].xrp();
// NOTE: Because preclaim evaluates against a static readview, it
// does not reflect fee deductions from other transactions paid by
// the same account within the current ledger.
// As a result, if an account's balance is over-committed across multiple
// transactions, this check may pass optimistically.
// The fee shortfall will be handled by the Transactor::reset mechanism,
// which caps the fee to the remaining actual balance.
if (balance < feePaid)
{
JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(balance)

View File

@@ -559,18 +559,23 @@ Payment::doApply()
// This is the total reserve in drops.
auto const reserve = view().fees().accountReserve(ownerCount);
// preFeeBalance_ is the balance on the sending account BEFORE the
// fees were charged. We want to make sure we have enough reserve
// to send. Allow final spend to use reserve for fee.
auto const mmm = std::max(reserve, ctx_.tx.getFieldAmount(sfFee).xrp());
// In a delegated payment, the fee payer is the delegated account,
// not the source account (account_).
bool const accountIsPayer = (ctx_.tx.getFeePayer() == account_);
if (preFeeBalance_ < dstAmount.xrp() + mmm)
// preFeeBalance_ is the balance on the source account (account_) BEFORE the fees
// were charged. If source account is the fee payer, it must also cover the fee.
// The final spend may use the reserve to cover fees.
auto const minRequiredFunds =
accountIsPayer ? std::max(reserve, ctx_.tx.getFieldAmount(sfFee).xrp()) : reserve;
if (preFeeBalance_ < dstAmount.xrp() + minRequiredFunds)
{
// Vote no. However the transaction might succeed, if applied in
// a different order.
JLOG(j_.trace()) << "Delay transaction: Insufficient funds: " << to_string(preFeeBalance_)
<< " / " << to_string(dstAmount.xrp() + mmm) << " (" << to_string(reserve)
<< ")";
<< " / " << to_string(dstAmount.xrp() + minRequiredFunds) << " ("
<< to_string(reserve) << ")";
return tecUNFUNDED_PAYMENT;
}

View File

@@ -43,10 +43,10 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
if (!vault)
return tecNO_ENTRY;
auto const assets = ctx.tx[sfAmount];
auto const amount = ctx.tx[sfAmount];
auto const vaultAsset = vault->at(sfAsset);
auto const vaultShare = vault->at(sfShareMPTID);
if (assets.asset() != vaultAsset && assets.asset() != vaultShare)
if (amount.asset() != vaultAsset && amount.asset() != vaultShare)
return tecWRONG_ASSET;
auto const& vaultAccount = vault->at(sfAccount);
@@ -67,8 +67,53 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
// LCOV_EXCL_STOP
}
if (auto const ret = canWithdraw(ctx.view, ctx.tx))
return ret;
if (ctx.view.rules().enabled(fixSecurity3_1_3) && amount.asset() == vaultShare)
{
// Post-fixSecurity3_1_3: if the user specified shares, convert
// to the equivalent asset amount before checking withdrawal
// limits. Pre-amendment the limit check was skipped for
// share-denominated withdrawals.
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(vaultShare));
if (!sleIssuance)
{
// LCOV_EXCL_START
JLOG(ctx.j.error()) << "VaultWithdraw: missing issuance of vault shares.";
return tefINTERNAL;
// LCOV_EXCL_STOP
}
try
{
auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, amount);
if (!maybeAssets)
return tefINTERNAL; // LCOV_EXCL_LINE
if (auto const ret = canWithdraw(
ctx.view,
account,
dstAcct,
*maybeAssets,
ctx.tx.isFieldPresent(sfDestinationTag)))
return ret;
}
catch (std::overflow_error const&)
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(ctx.j.debug()) //
<< "VaultWithdraw: overflow error with"
<< " scale=" << (int)vault->at(sfScale) //
<< ", assetsTotal=" << vault->at(sfAssetsTotal)
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
<< ", amount=" << amount.value();
return tecPATH_DRY;
}
}
else
{
if (auto const ret = canWithdraw(ctx.view, ctx.tx))
return ret;
}
// If sending to Account (i.e. not a transfer), we will also create (only
// if authorized) a trust line or MPToken as needed, in doApply().

View File

@@ -274,37 +274,42 @@ class Delegate_test : public beast::unit_test::suite
testcase("test fee");
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, carol);
env.fund(XRP(1000), bob);
env.close();
// Common setup: fund alice, bob, carol with 1000 XRP.
auto setup = [&](Env& env) {
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(1000), alice, bob, carol);
env.close();
return std::make_tuple(alice, bob, carol);
};
// No fee deduction for terNO_DELEGATE_PERMISSION.
{
auto aliceBalance = env.balance(alice);
auto bobBalance = env.balance(bob);
auto carolBalance = env.balance(carol);
Env env(*this);
auto [alice, bob, carol] = setup(env);
env(pay(alice, carol, XRP(100)),
fee(XRP(2000)),
delegate::as(bob),
ter(terNO_DELEGATE_PERMISSION));
auto const aliceBalance = env.balance(alice);
auto const bobBalance = env.balance(bob);
auto const carolBalance = env.balance(carol);
env(pay(alice, carol, XRP(100)), delegate::as(bob), ter(terNO_DELEGATE_PERMISSION));
env.close();
BEAST_EXPECT(env.balance(alice) == aliceBalance);
BEAST_EXPECT(env.balance(bob) == bobBalance);
BEAST_EXPECT(env.balance(carol) == carolBalance);
}
env(delegate::set(alice, bob, {"Payment"}));
env.close();
// Delegate pays the fee successfully.
{
// Delegate pays the fee
auto aliceBalance = env.balance(alice);
auto bobBalance = env.balance(bob);
auto carolBalance = env.balance(carol);
Env env(*this);
auto [alice, bob, carol] = setup(env);
env(delegate::set(alice, bob, {"Payment"}));
env.close();
auto const aliceBalance = env.balance(alice);
auto const bobBalance = env.balance(bob);
auto const carolBalance = env.balance(carol);
auto const sendAmt = XRP(100);
auto const feeAmt = XRP(10);
@@ -315,11 +320,16 @@ class Delegate_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(carol) == carolBalance + sendAmt);
}
// Bob has insufficient balance to pay the fee, will get terINSUF_FEE_B.
{
// insufficient balance to pay fee
auto aliceBalance = env.balance(alice);
auto bobBalance = env.balance(bob);
auto carolBalance = env.balance(carol);
Env env(*this);
auto [alice, bob, carol] = setup(env);
env(delegate::set(alice, bob, {"Payment"}));
env.close();
auto const aliceBalance = env.balance(alice);
auto const bobBalance = env.balance(bob);
auto const carolBalance = env.balance(carol);
env(pay(alice, carol, XRP(100)),
fee(XRP(2000)),
@@ -331,22 +341,143 @@ class Delegate_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(carol) == carolBalance);
}
// The delegated account has enough balance to pay and delegator has enough reserve
{
// fee is paid by Delegate
// on context reset (tec error)
auto aliceBalance = env.balance(alice);
auto bobBalance = env.balance(bob);
auto carolBalance = env.balance(carol);
auto const feeAmt = XRP(10);
// Common setup: fund accounts and grant Bob permission to pay on Alice's behalf.
// Alice is funded with exactly (paymentAmount + reserve + baseFee): baseFee covers
// the DelegateSet tx cost, leaving Alice with exactly (paymentAmount + reserve).
// highFee = reserve + baseFee, strictly greater than reserve, so that
// max(reserve, highFee) = highFee — making the direct payment check fail.
auto setup = [&](Env& env) {
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env(pay(alice, carol, XRP(20000)),
fee(feeAmt),
delegate::as(bob),
ter(tecUNFUNDED_PAYMENT));
auto const baseFee = env.current()->fees().base;
auto const reserve = env.current()->fees().accountReserve(1);
auto const paymentAmount = XRP(1);
auto const highFee = reserve + baseFee;
BEAST_EXPECT(highFee > reserve);
env.fund(paymentAmount + reserve + baseFee, alice);
env.fund(XRP(1000), bob);
env.fund(XRP(1000), carol);
env.close();
env(delegate::set(alice, bob, {"Payment"}));
env.close();
env.require(balance(alice, paymentAmount + reserve));
return std::make_tuple(alice, bob, carol, paymentAmount, highFee, reserve);
};
// Alice's balance (paymentAmount + reserve) is insufficient to cover both
// the payment and highFee directly. Even though fees are allowed to dip
// below reserve, when Alice pays the fee herself the required funds =
// paymentAmount + max(reserve, highFee) = paymentAmount + highFee
// (since highFee > reserve), which still exceeds her balance.
// tec: highFee is consumed from Alice's balance.
{
Env env(*this);
auto [alice, bob, carol, paymentAmount, highFee, reserve] = setup(env);
auto const aliceBalance = env.balance(alice);
auto const bobBalance = env.balance(bob);
auto const carolBalance = env.balance(carol);
env(pay(alice, carol, paymentAmount), fee(highFee), ter(tecUNFUNDED_PAYMENT));
// tec consumes the fee from Alice; carol and bob are unaffected.
BEAST_EXPECT(env.balance(alice) == aliceBalance - highFee);
BEAST_EXPECT(env.balance(bob) == bobBalance);
BEAST_EXPECT(env.balance(carol) == carolBalance);
}
// The payment succeeds because the delegated account pays the fee.
// Alice only needs (paymentAmount + reserve).
{
Env env(*this);
auto [alice, bob, carol, paymentAmount, highFee, reserve] = setup(env);
auto const alicePrePay = env.balance(alice, XRP);
auto const bobPrePay = env.balance(bob, XRP);
auto const carolPrePay = env.balance(carol, XRP);
env(pay(alice, carol, paymentAmount), delegate::as(bob), fee(highFee));
env.close();
env.require(balance(alice, alicePrePay - paymentAmount));
env.require(balance(bob, bobPrePay - highFee));
env.require(balance(carol, carolPrePay + paymentAmount));
}
}
// Delegated account can pay the fee even if it dips below reserve.
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
auto const baseFee = env.current()->fees().base;
auto const baseReserve = env.current()->fees().accountReserve(0);
env.fund(env.current()->fees().accountReserve(1) + baseFee + XRP(1), alice);
env.fund(baseReserve, bob);
env.fund(XRP(1000), carol);
env.close();
BEAST_EXPECT(env.balance(alice) == aliceBalance);
BEAST_EXPECT(env.balance(bob) == bobBalance - feeAmt);
BEAST_EXPECT(env.balance(carol) == carolBalance);
env(delegate::set(alice, bob, {"Payment"}));
env.close();
auto const alicePreTx = env.balance(alice, XRP);
auto const bobPreTx = env.balance(bob, XRP);
// After paying for this transaction, bob's balance will
// dip below the base reserve
env(pay(alice, carol, XRP(1)), delegate::as(bob));
env.close();
// Bob's balance is now less than the base reserve.
BEAST_EXPECT(env.balance(bob, XRP) < baseReserve);
env.require(balance(bob, bobPreTx - drops(baseFee)));
// Alice's balance only decreased by the 1.0 XRP she sent.
env.require(balance(alice, alicePreTx - XRP(1)));
}
// The delegated account has enough balance for the fee, but delegator
// runs into tecUNFUNDED_PAYMENT.
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
auto const baseFee = env.current()->fees().base;
auto const reserve = env.current()->fees().accountReserve(1);
// Alice is funded with (reserve + baseFee): after DelegateSet she has
// exactly 'reserve', which is insufficient to send XRP(10) while keeping
// reserve. Bob has plenty to pay the fee.
env.fund(reserve + baseFee, alice);
env.fund(XRP(1000), bob);
env.fund(XRP(1000), carol);
env.close();
env(delegate::set(alice, bob, {"Payment"}));
env.close();
auto const alicePrePay = env.balance(alice, XRP);
auto const bobPrePay = env.balance(bob, XRP);
auto const carolPrePay = env.balance(carol, XRP);
// Bob pays the fee, but Alice has insufficient balance to send XRP(10).
env(pay(alice, carol, XRP(10)), delegate::as(bob), ter(tecUNFUNDED_PAYMENT));
env.require(balance(alice, alicePrePay));
env.require(balance(bob, bobPrePay - drops(baseFee)));
env.require(balance(carol, carolPrePay));
}
}
@@ -1238,8 +1369,8 @@ class Delegate_test : public beast::unit_test::suite
// test MPTokenIssuanceUnlock and MPTokenIssuanceLock permissions
{
Env env(*this);
Account alice{"alice"};
Account bob{"bob"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(100000), alice, bob);
env.close();
@@ -1285,8 +1416,8 @@ class Delegate_test : public beast::unit_test::suite
// test mix of granular and transaction level permission
{
Env env(*this);
Account alice{"alice"};
Account bob{"bob"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(100000), alice, bob);
env.close();
@@ -1332,8 +1463,8 @@ class Delegate_test : public beast::unit_test::suite
// tfFullyCanonicalSig won't block delegated transaction
{
Env env(*this);
Account alice{"alice"};
Account bob{"bob"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(100000), alice, bob);
env.close();
@@ -1410,11 +1541,9 @@ class Delegate_test : public beast::unit_test::suite
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(100000), alice, bob, carol);
env.close();
@@ -1448,11 +1577,9 @@ class Delegate_test : public beast::unit_test::suite
{
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(100000), alice, bob, carol);
env.close();
@@ -1567,8 +1694,8 @@ class Delegate_test : public beast::unit_test::suite
Env env(*this, features);
Account alice{"alice"};
Account bob{"bob"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(100000), alice, bob);
env.close();

View File

@@ -5231,6 +5231,102 @@ class Vault_test : public beast::unit_test::suite
}
}
// Reproduction: canWithdraw IOU limit check bypassed when
// withdrawal amount is specified in shares (MPT) rather than in assets.
void
testBug6_LimitBypassWithShares()
{
using namespace test::jtx;
testcase("Bug6 - limit bypass with share-denominated withdrawal");
auto const allAmendments = testable_amendments() | featureSingleAssetVault;
for (auto const& features : {allAmendments, allAmendments - fixSecurity3_1_3})
{
bool const withFix = features[fixSecurity3_1_3];
Env env{*this, features};
Account const owner{"owner"};
Account const issuer{"issuer"};
Account const depositor{"depositor"};
Account const charlie{"charlie"};
Vault const vault{env};
env.fund(XRP(1000), issuer, owner, depositor, charlie);
env(fset(issuer, asfAllowTrustLineClawback));
env.close();
PrettyAsset const asset = issuer["IOU"];
env.trust(asset(1000), owner);
env.trust(asset(1000), depositor);
env(pay(issuer, owner, asset(200)));
env(pay(issuer, depositor, asset(200)));
env.close();
// Charlie gets a LOW trustline limit of 5
env.trust(asset(5), charlie);
env.close();
auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
auto const depositTx =
vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
env(depositTx);
env.close();
// Get the share MPT info
auto const vaultSle = env.le(keylet);
if (!BEAST_EXPECT(vaultSle))
return;
auto const mptIssuanceID = vaultSle->at(sfShareMPTID);
MPTIssue const shares(mptIssuanceID);
PrettyAsset const share(shares);
// CONTROL: Withdraw 10 IOU (asset-denominated) to charlie.
// Charlie's limit is 5, so this should be rejected with tecNO_LINE
// regardless of the amendment.
{
auto withdrawTx =
vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
withdrawTx[sfDestination] = charlie.human();
env(withdrawTx, ter{tecNO_LINE});
env.close();
}
auto const charlieBalanceBefore = env.balance(charlie, asset.raw().get<Issue>());
// Withdraw the equivalent amount in shares to charlie.
// Post-fix: rejected (tecNO_LINE) because the share amount is
// converted to assets and the trustline limit is checked.
// Pre-fix: succeeds (tesSUCCESS) because the limit check was
// skipped for share-denominated withdrawals.
{
auto withdrawTx = vault.withdraw(
{.depositor = depositor,
.id = keylet.key,
.amount = STAmount(share, 10'000'000)});
withdrawTx[sfDestination] = charlie.human();
env(withdrawTx, ter{withFix ? TER{tecNO_LINE} : TER{tesSUCCESS}});
env.close();
auto const charlieBalanceAfter = env.balance(charlie, asset.raw().get<Issue>());
if (withFix)
{
// Post-fix: charlie's balance is unchanged — the withdrawal
// was correctly rejected despite being share-denominated.
BEAST_EXPECT(charlieBalanceAfter == charlieBalanceBefore);
}
else
{
// Pre-fix: charlie received the assets, bypassing the
// trustline limit.
BEAST_EXPECT(charlieBalanceAfter > charlieBalanceBefore);
}
}
}
}
public:
void
run() override
@@ -5251,6 +5347,7 @@ public:
testVaultClawbackBurnShares();
testVaultClawbackAssets();
testAssetsMaximum();
testBug6_LimitBypassWithShares();
}
};

View File

@@ -127,20 +127,52 @@ class AccountTx_test : public beast::unit_test::suite
{
auto const& payment = j[jss::result][jss::transactions][1u];
return (payment.isMember(jss::tx_json)) &&
(payment[jss::tx_json][jss::TransactionType] == jss::Payment) &&
(payment[jss::tx_json][jss::DeliverMax] == "10000000010") &&
(!payment[jss::tx_json].isMember(jss::Amount)) &&
(!payment[jss::tx_json].isMember(jss::hash)) &&
(payment[jss::hash] ==
"9F3085D85F472D1CC29627F260DF68EDE59D42D1D0C33E345"
"ECF0D4CE981D0A8") &&
(payment[jss::validated] == true) &&
(payment[jss::ledger_index] == 3) &&
(payment[jss::ledger_hash] ==
"5476DCD816EA04CBBA57D47BBF1FC58A5217CC93A5ADD79CB"
"580A5AFDD727E33") &&
(payment[jss::close_time_iso] == "2000-01-01T00:00:10Z");
if (apiVersion >= 3)
{
// In API v3, server-added lower-case fields must
// not be in tx_json, but must be at result level
return (payment.isMember(jss::tx_json)) &&
(payment[jss::tx_json][jss::TransactionType] == jss::Payment) &&
(payment[jss::tx_json][jss::DeliverMax] == "10000000010") &&
(!payment[jss::tx_json].isMember(jss::Amount)) &&
(!payment[jss::tx_json].isMember(jss::hash)) &&
(!payment[jss::tx_json].isMember(jss::date)) &&
(!payment[jss::tx_json].isMember(jss::ledger_index)) &&
(!payment[jss::tx_json].isMember(jss::ctid)) &&
// date and ctid must be at the transaction
// object level (outside tx_json) in API v3
(payment.isMember(jss::date)) && (payment.isMember(jss::ctid)) &&
(payment[jss::hash] ==
"9F3085D85F472D1CC29627F260DF68EDE59D42D1D0C33E345"
"ECF0D4CE981D0A8") &&
(payment[jss::validated] == true) &&
(payment[jss::ledger_index] == 3) &&
(payment[jss::ledger_hash] ==
"5476DCD816EA04CBBA57D47BBF1FC58A5217CC93A5ADD79CB"
"580A5AFDD727E33") &&
(payment[jss::close_time_iso] == "2000-01-01T00:00:10Z");
}
else
{
// In API v2, date and ledger_index are still in
// tx_json for backwards compatibility
return (payment.isMember(jss::tx_json)) &&
(payment[jss::tx_json][jss::TransactionType] == jss::Payment) &&
(payment[jss::tx_json][jss::DeliverMax] == "10000000010") &&
(!payment[jss::tx_json].isMember(jss::Amount)) &&
(!payment[jss::tx_json].isMember(jss::hash)) &&
(payment[jss::tx_json].isMember(jss::date)) &&
(payment[jss::tx_json].isMember(jss::ledger_index)) &&
(payment[jss::hash] ==
"9F3085D85F472D1CC29627F260DF68EDE59D42D1D0C33E345"
"ECF0D4CE981D0A8") &&
(payment[jss::validated] == true) &&
(payment[jss::ledger_index] == 3) &&
(payment[jss::ledger_hash] ==
"5476DCD816EA04CBBA57D47BBF1FC58A5217CC93A5ADD79CB"
"580A5AFDD727E33") &&
(payment[jss::close_time_iso] == "2000-01-01T00:00:10Z");
}
}
else
{

View File

@@ -1,7 +1,7 @@
#include <test/jtx/TestSuite.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/handlers/WalletPropose.h>
#include <xrpld/rpc/handlers/admin/keygen/WalletPropose.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/json_writer.h>

View File

@@ -775,6 +775,25 @@ class Transaction_test : public beast::unit_test::suite
result[jss::result][jss::ledger_hash] ==
"B41882E20F0EC6228417D28B9AE0F33833645D35F6799DFB782AC97FC4BB51"
"D2");
auto const& tx_json = result[jss::result][jss::tx_json];
if (apiVersion >= 3)
{
// In API v3, server-added lower-case fields must not appear
// inside tx_json; they are at the result level.
BEAST_EXPECT(!tx_json.isMember(jss::date));
BEAST_EXPECT(!tx_json.isMember(jss::ledger_index));
BEAST_EXPECT(!tx_json.isMember(jss::ctid));
// date must be at result level in API v3
BEAST_EXPECT(result[jss::result].isMember(jss::date));
}
else
{
// In API v2, date and ledger_index are still included in
// tx_json for backwards compatibility.
BEAST_EXPECT(tx_json.isMember(jss::date));
BEAST_EXPECT(tx_json.isMember(jss::ledger_index));
}
}
for (auto memberIt = expected.begin(); memberIt != expected.end(); memberIt++)

View File

@@ -141,28 +141,30 @@ Transaction::getJson(JsonOptions options, bool binary) const
ret[jss::inLedger] = mLedgerIndex;
}
// TODO: disable_API_prior_V3 to disable output of both `date` and
// `ledger_index` elements (taking precedence over include_date)
ret[jss::ledger_index] = mLedgerIndex;
if (options & JsonOptions::include_date)
if (!(options & JsonOptions::disable_API_prior_V3))
{
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mLedgerIndex);
if (ct)
ret[jss::date] = ct->time_since_epoch().count();
}
ret[jss::ledger_index] = mLedgerIndex;
// compute outgoing CTID
// override local network id if it's explicitly in the txn
std::optional netID = mNetworkID;
if (mTransaction->isFieldPresent(sfNetworkID))
netID = mTransaction->getFieldU32(sfNetworkID);
if (options & JsonOptions::include_date)
{
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mLedgerIndex);
if (ct)
ret[jss::date] = ct->time_since_epoch().count();
}
if (mTxnSeq && netID)
{
std::optional<std::string> const ctid = RPC::encodeCTID(mLedgerIndex, *mTxnSeq, *netID);
if (ctid)
ret[jss::ctid] = *ctid;
// compute outgoing CTID
// override local network id if it's explicitly in the txn
std::optional netID = mNetworkID;
if (mTransaction->isFieldPresent(sfNetworkID))
netID = mTransaction->getFieldU32(sfNetworkID);
if (mTxnSeq && netID)
{
std::optional<std::string> const ctid =
RPC::encodeCTID(mLedgerIndex, *mTxnSeq, *netID);
if (ctid)
ret[jss::ctid] = *ctid;
}
}
}

View File

@@ -7,7 +7,7 @@
#include <xrpld/overlay/detail/Tuning.h>
#include <xrpld/overlay/predicates.h>
#include <xrpld/peerfinder/make_Manager.h>
#include <xrpld/rpc/handlers/GetCounts.h>
#include <xrpld/rpc/handlers/admin/status/GetCounts.h>
#include <xrpld/rpc/json_body.h>
#include <xrpl/basics/base64.h>

View File

@@ -1,6 +1,6 @@
#include <xrpld/rpc/detail/Handler.h>
#include <xrpld/rpc/handlers/Handlers.h>
#include <xrpld/rpc/handlers/Version.h>
#include <xrpld/rpc/handlers/server_info/Version.h>
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/ApiVersion.h>
@@ -75,7 +75,7 @@ Handler const handlerArray[]{
{"account_nfts", byRef(&doAccountNFTs), Role::USER, NO_CONDITION},
{"account_objects", byRef(&doAccountObjects), Role::USER, NO_CONDITION},
{"account_offers", byRef(&doAccountOffers), Role::USER, NO_CONDITION},
{"account_tx", byRef(&doAccountTxJson), Role::USER, NO_CONDITION},
{"account_tx", byRef(&doAccountTx), Role::USER, NO_CONDITION},
{"amm_info", byRef(&doAMMInfo), Role::USER, NO_CONDITION},
{"blacklist", byRef(&doBlackList), Role::ADMIN, NO_CONDITION},
{"book_changes", byRef(&doBookChanges), Role::USER, NO_CONDITION},

View File

@@ -0,0 +1,71 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/jss.h>
#include <optional>
namespace xrpl {
// {
// public_key: <public_key>
// channel_id: 256-bit channel id
// drops: 64-bit uint (as string)
// signature: signature to verify
// }
Json::Value
doChannelVerify(RPC::JsonContext& context)
{
auto const& params(context.params);
for (auto const& p : {jss::public_key, jss::channel_id, jss::amount, jss::signature})
{
if (!params.isMember(p))
return RPC::missing_field_error(p);
}
std::optional<PublicKey> pk;
{
std::string const strPk = params[jss::public_key].asString();
pk = parseBase58<PublicKey>(TokenType::AccountPublic, strPk);
if (!pk)
{
auto pkHex = strUnHex(strPk);
if (!pkHex)
return rpcError(rpcPUBLIC_MALFORMED);
auto const pkType = publicKeyType(makeSlice(*pkHex));
if (!pkType)
return rpcError(rpcPUBLIC_MALFORMED);
pk.emplace(makeSlice(*pkHex));
}
}
uint256 channelId;
if (!channelId.parseHex(params[jss::channel_id].asString()))
return rpcError(rpcCHANNEL_MALFORMED);
std::optional<std::uint64_t> const optDrops =
params[jss::amount].isString() ? to_uint64(params[jss::amount].asString()) : std::nullopt;
if (!optDrops)
return rpcError(rpcCHANNEL_AMT_MALFORMED);
std::uint64_t const drops = *optDrops;
auto sig = strUnHex(params[jss::signature].asString());
if (!sig || sig->empty())
return rpcError(rpcINVALID_PARAMS);
Serializer msg;
serializePayChanAuthorization(msg, channelId, XRPAmount(drops));
Json::Value result;
result[jss::signature_verified] = verify(*pk, msg.slice(), makeSlice(*sig));
return result;
}
} // namespace xrpl

View File

@@ -1,6 +1,6 @@
#pragma once
#include <xrpld/rpc/handlers/LedgerHandler.h>
#include <xrpld/rpc/handlers/ledger/Ledger.h>
namespace xrpl {
@@ -19,7 +19,7 @@ doAccountObjects(RPC::JsonContext&);
Json::Value
doAccountOffers(RPC::JsonContext&);
Json::Value
doAccountTxJson(RPC::JsonContext&);
doAccountTx(RPC::JsonContext&);
Json::Value
doAMMInfo(RPC::JsonContext&);
Json::Value

View File

@@ -0,0 +1,160 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/nftPageMask.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/tx/transactors/nft/NFTokenUtils.h>
namespace xrpl {
/** General RPC command that can retrieve objects in the account root.
{
account: <account>
ledger_hash: <string> // optional
ledger_index: <string | unsigned integer> // optional
type: <string> // optional, defaults to all account objects types
limit: <integer> // optional
marker: <opaque> // optional, resume previous query
}
*/
Json::Value
doAccountNFTs(RPC::JsonContext& context)
{
auto const& params = context.params;
if (!params.isMember(jss::account))
return RPC::missing_field_error(jss::account);
if (!params[jss::account].isString())
return RPC::invalid_field_error(jss::account);
auto id = parseBase58<AccountID>(params[jss::account].asString());
if (!id)
{
return rpcError(rpcACT_MALFORMED);
}
std::shared_ptr<ReadView const> ledger;
auto result = RPC::lookupLedger(ledger, context);
if (ledger == nullptr)
return result;
auto const accountID{id.value()};
if (!ledger->exists(keylet::account(accountID)))
return rpcError(rpcACT_NOT_FOUND);
unsigned int limit = 0;
if (auto err = readLimitField(limit, RPC::Tuning::accountNFTokens, context))
return *err;
uint256 marker;
bool const markerSet = params.isMember(jss::marker);
if (markerSet)
{
auto const& m = params[jss::marker];
if (!m.isString())
return RPC::expected_field_error(jss::marker, "string");
if (!marker.parseHex(m.asString()))
return RPC::invalid_field_error(jss::marker);
}
auto const first = keylet::nftpage(keylet::nftpage_min(accountID), marker);
auto const last = keylet::nftpage_max(accountID);
auto cp = ledger->read(
Keylet(ltNFTOKEN_PAGE, ledger->succ(first.key, last.key.next()).value_or(last.key)));
std::uint32_t cnt = 0;
auto& nfts = (result[jss::account_nfts] = Json::arrayValue);
// Continue iteration from the current page:
bool pastMarker = marker.isZero();
bool markerFound = false;
uint256 const maskedMarker = marker & nft::pageMask;
while (cp)
{
auto arr = cp->getFieldArray(sfNFTokens);
for (auto const& o : arr)
{
// Scrolling past the marker gets weird. We need to look at
// a couple of conditions.
//
// 1. If the low 96-bits don't match, then we compare only
// against the low 96-bits, since that's what determines
// the sort order of the pages.
//
// 2. However, within one page there can be a number of
// NFTokenIDs that all have the same low 96 bits. If we're
// in that case then we need to compare against the full
// 256 bits.
uint256 const nftokenID = o[sfNFTokenID];
uint256 const maskedNftokenID = nftokenID & nft::pageMask;
if (!pastMarker)
{
if (maskedNftokenID < maskedMarker)
continue;
if (maskedNftokenID == maskedMarker && nftokenID < marker)
continue;
if (nftokenID == marker)
{
markerFound = true;
continue;
}
}
if (markerSet && !markerFound)
return RPC::invalid_field_error(jss::marker);
pastMarker = true;
{
Json::Value& obj = nfts.append(o.getJson(JsonOptions::none));
// Pull out the components of the nft ID.
obj[sfFlags.jsonName] = nft::getFlags(nftokenID);
obj[sfIssuer.jsonName] = to_string(nft::getIssuer(nftokenID));
obj[sfNFTokenTaxon.jsonName] = nft::toUInt32(nft::getTaxon(nftokenID));
obj[jss::nft_serial] = nft::getSerial(nftokenID);
if (std::uint16_t const xferFee = {nft::getTransferFee(nftokenID)})
obj[sfTransferFee.jsonName] = xferFee;
}
if (++cnt == limit)
{
result[jss::limit] = limit;
result[jss::marker] = to_string(o.getFieldH256(sfNFTokenID));
return result;
}
}
if (auto npm = (*cp)[~sfNextPageMin])
{
cp = ledger->read(Keylet(ltNFTOKEN_PAGE, *npm));
}
else
{
cp = nullptr;
}
}
if (markerSet && !markerFound)
return RPC::invalid_field_error(jss::marker);
result[jss::account] = toBase58(accountID);
context.loadType = Resource::feeMediumBurdenRPC;
return result;
}
} // namespace xrpl

View File

@@ -11,156 +11,11 @@
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/nftPageMask.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/tx/transactors/nft/NFTokenUtils.h>
#include <string>
namespace xrpl {
/** General RPC command that can retrieve objects in the account root.
{
account: <account>
ledger_hash: <string> // optional
ledger_index: <string | unsigned integer> // optional
type: <string> // optional, defaults to all account objects types
limit: <integer> // optional
marker: <opaque> // optional, resume previous query
}
*/
Json::Value
doAccountNFTs(RPC::JsonContext& context)
{
auto const& params = context.params;
if (!params.isMember(jss::account))
return RPC::missing_field_error(jss::account);
if (!params[jss::account].isString())
return RPC::invalid_field_error(jss::account);
auto id = parseBase58<AccountID>(params[jss::account].asString());
if (!id)
{
return rpcError(rpcACT_MALFORMED);
}
std::shared_ptr<ReadView const> ledger;
auto result = RPC::lookupLedger(ledger, context);
if (ledger == nullptr)
return result;
auto const accountID{id.value()};
if (!ledger->exists(keylet::account(accountID)))
return rpcError(rpcACT_NOT_FOUND);
unsigned int limit = 0;
if (auto err = readLimitField(limit, RPC::Tuning::accountNFTokens, context))
return *err;
uint256 marker;
bool const markerSet = params.isMember(jss::marker);
if (markerSet)
{
auto const& m = params[jss::marker];
if (!m.isString())
return RPC::expected_field_error(jss::marker, "string");
if (!marker.parseHex(m.asString()))
return RPC::invalid_field_error(jss::marker);
}
auto const first = keylet::nftpage(keylet::nftpage_min(accountID), marker);
auto const last = keylet::nftpage_max(accountID);
auto cp = ledger->read(
Keylet(ltNFTOKEN_PAGE, ledger->succ(first.key, last.key.next()).value_or(last.key)));
std::uint32_t cnt = 0;
auto& nfts = (result[jss::account_nfts] = Json::arrayValue);
// Continue iteration from the current page:
bool pastMarker = marker.isZero();
bool markerFound = false;
uint256 const maskedMarker = marker & nft::pageMask;
while (cp)
{
auto arr = cp->getFieldArray(sfNFTokens);
for (auto const& o : arr)
{
// Scrolling past the marker gets weird. We need to look at
// a couple of conditions.
//
// 1. If the low 96-bits don't match, then we compare only
// against the low 96-bits, since that's what determines
// the sort order of the pages.
//
// 2. However, within one page there can be a number of
// NFTokenIDs that all have the same low 96 bits. If we're
// in that case then we need to compare against the full
// 256 bits.
uint256 const nftokenID = o[sfNFTokenID];
uint256 const maskedNftokenID = nftokenID & nft::pageMask;
if (!pastMarker)
{
if (maskedNftokenID < maskedMarker)
continue;
if (maskedNftokenID == maskedMarker && nftokenID < marker)
continue;
if (nftokenID == marker)
{
markerFound = true;
continue;
}
}
if (markerSet && !markerFound)
return RPC::invalid_field_error(jss::marker);
pastMarker = true;
{
Json::Value& obj = nfts.append(o.getJson(JsonOptions::none));
// Pull out the components of the nft ID.
obj[sfFlags.jsonName] = nft::getFlags(nftokenID);
obj[sfIssuer.jsonName] = to_string(nft::getIssuer(nftokenID));
obj[sfNFTokenTaxon.jsonName] = nft::toUInt32(nft::getTaxon(nftokenID));
obj[jss::nft_serial] = nft::getSerial(nftokenID);
if (std::uint16_t const xferFee = {nft::getTransferFee(nftokenID)})
obj[sfTransferFee.jsonName] = xferFee;
}
if (++cnt == limit)
{
result[jss::limit] = limit;
result[jss::marker] = to_string(o.getFieldH256(sfNFTokenID));
return result;
}
}
if (auto npm = (*cp)[~sfNextPageMin])
{
cp = ledger->read(Keylet(ltNFTOKEN_PAGE, *npm));
}
else
{
cp = nullptr;
}
}
if (markerSet && !markerFound)
return RPC::invalid_field_error(jss::marker);
result[jss::account] = toBase58(accountID);
context.loadType = Resource::feeMediumBurdenRPC;
return result;
}
/** Gathers all objects for an account in a ledger.
@param ledger Ledger to search account objects.
@param account AccountID to find objects for.

View File

@@ -3,6 +3,7 @@
#include <xrpld/app/misc/DeliverMax.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/rpc/CTID.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/DeliveredAmount.h>
#include <xrpld/rpc/MPTokenIssuanceID.h>
@@ -11,6 +12,7 @@
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ErrorCodes.h>
@@ -289,8 +291,10 @@ populateJsonResponse(
auto const json_tx = (context.apiVersion > 1 ? jss::tx_json : jss::tx);
if (context.apiVersion > 1)
{
jvObj[json_tx] = txn->getJson(
JsonOptions::include_date | JsonOptions::disable_API_prior_V2, false);
auto const opts = context.apiVersion >= 3
? JsonOptions::disable_API_prior_V2 | JsonOptions::disable_API_prior_V3
: JsonOptions::include_date | JsonOptions::disable_API_prior_V2;
jvObj[json_tx] = txn->getJson(opts, false);
jvObj[jss::hash] = to_string(txn->getID());
jvObj[jss::ledger_index] = txn->getLedger();
jvObj[jss::ledger_hash] =
@@ -298,7 +302,20 @@ populateJsonResponse(
if (auto closeTime =
context.ledgerMaster.getCloseTimeBySeq(txn->getLedger()))
{
jvObj[jss::close_time_iso] = to_string_iso(*closeTime);
if (context.apiVersion >= 3)
jvObj[jss::date] = closeTime->time_since_epoch().count();
}
if (context.apiVersion >= 3 && txnMeta)
{
uint32_t const lgrSeq = txn->getLedger();
uint32_t const txnIdx = txnMeta->getIndex();
uint32_t const netID = context.app.getNetworkIDService().getNetworkID();
if (auto const ctid = RPC::encodeCTID(lgrSeq, txnIdx, netID))
jvObj[jss::ctid] = *ctid;
}
}
else
{
@@ -364,7 +381,7 @@ populateJsonResponse(
// resume previous query
// }
Json::Value
doAccountTxJson(RPC::JsonContext& context)
doAccountTx(RPC::JsonContext& context)
{
if (!context.app.config().useTxTables())
return rpcError(rpcNOT_ENABLED);

View File

@@ -1,6 +1,6 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/handlers/WalletPropose.h>
#include <xrpld/rpc/handlers/admin/keygen/WalletPropose.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/ErrorCodes.h>

View File

@@ -9,7 +9,6 @@
#include <optional>
#include <string>
#include <utility>
namespace xrpl {
@@ -66,46 +65,4 @@ doPeerReservationsAdd(RPC::JsonContext& context)
return result;
}
Json::Value
doPeerReservationsDel(RPC::JsonContext& context)
{
auto const& params = context.params;
// We repeat much of the parameter parsing from `doPeerReservationsAdd`.
if (!params.isMember(jss::public_key))
return RPC::missing_field_error(jss::public_key);
if (!params[jss::public_key].isString())
return RPC::expected_field_error(jss::public_key, "a string");
std::optional<PublicKey> optPk =
parseBase58<PublicKey>(TokenType::NodePublic, params[jss::public_key].asString());
if (!optPk)
return rpcError(rpcPUBLIC_MALFORMED);
PublicKey const& nodeId = *optPk;
auto const previous = context.app.getPeerReservations().erase(nodeId);
Json::Value result{Json::objectValue};
if (previous)
{
result[jss::previous] = previous->toJson();
}
return result;
}
Json::Value
doPeerReservationsList(RPC::JsonContext& context)
{
auto const& reservations = context.app.getPeerReservations().list();
// Enumerate the reservations in context.app.getPeerReservations()
// as a Json::Value.
Json::Value result{Json::objectValue};
Json::Value& jaReservations = result[jss::reservations] = Json::arrayValue;
for (auto const& reservation : reservations)
{
jaReservations.append(reservation.toJson());
}
return result;
}
} // namespace xrpl

View File

@@ -0,0 +1,41 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/handlers/Handlers.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/jss.h>
#include <optional>
namespace xrpl {
Json::Value
doPeerReservationsDel(RPC::JsonContext& context)
{
auto const& params = context.params;
// We repeat much of the parameter parsing from `doPeerReservationsAdd`.
if (!params.isMember(jss::public_key))
return RPC::missing_field_error(jss::public_key);
if (!params[jss::public_key].isString())
return RPC::expected_field_error(jss::public_key, "a string");
std::optional<PublicKey> optPk =
parseBase58<PublicKey>(TokenType::NodePublic, params[jss::public_key].asString());
if (!optPk)
return rpcError(rpcPUBLIC_MALFORMED);
PublicKey const& nodeId = *optPk;
auto const previous = context.app.getPeerReservations().erase(nodeId);
Json::Value result{Json::objectValue};
if (previous)
{
result[jss::previous] = previous->toJson();
}
return result;
}
} // namespace xrpl

View File

@@ -0,0 +1,24 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/handlers/Handlers.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/jss.h>
namespace xrpl {
Json::Value
doPeerReservationsList(RPC::JsonContext& context)
{
auto const& reservations = context.app.getPeerReservations().list();
// Enumerate the reservations in context.app.getPeerReservations()
// as a Json::Value.
Json::Value result{Json::objectValue};
Json::Value& jaReservations = result[jss::reservations] = Json::arrayValue;
for (auto const& reservation : reservations)
{
jaReservations.append(reservation.toJson());
}
return result;
}
} // namespace xrpl

View File

@@ -3,7 +3,6 @@
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/RPCErr.h>
@@ -83,61 +82,4 @@ doChannelAuthorize(RPC::JsonContext& context)
return result;
}
// {
// public_key: <public_key>
// channel_id: 256-bit channel id
// drops: 64-bit uint (as string)
// signature: signature to verify
// }
Json::Value
doChannelVerify(RPC::JsonContext& context)
{
auto const& params(context.params);
for (auto const& p : {jss::public_key, jss::channel_id, jss::amount, jss::signature})
{
if (!params.isMember(p))
return RPC::missing_field_error(p);
}
std::optional<PublicKey> pk;
{
std::string const strPk = params[jss::public_key].asString();
pk = parseBase58<PublicKey>(TokenType::AccountPublic, strPk);
if (!pk)
{
auto pkHex = strUnHex(strPk);
if (!pkHex)
return rpcError(rpcPUBLIC_MALFORMED);
auto const pkType = publicKeyType(makeSlice(*pkHex));
if (!pkType)
return rpcError(rpcPUBLIC_MALFORMED);
pk.emplace(makeSlice(*pkHex));
}
}
uint256 channelId;
if (!channelId.parseHex(params[jss::channel_id].asString()))
return rpcError(rpcCHANNEL_MALFORMED);
std::optional<std::uint64_t> const optDrops =
params[jss::amount].isString() ? to_uint64(params[jss::amount].asString()) : std::nullopt;
if (!optDrops)
return rpcError(rpcCHANNEL_AMT_MALFORMED);
std::uint64_t const drops = *optDrops;
auto sig = strUnHex(params[jss::signature].asString());
if (!sig || sig->empty())
return rpcError(rpcINVALID_PARAMS);
Serializer msg;
serializePayChanAuthorization(msg, channelId, XRPAmount(drops));
Json::Value result;
result[jss::signature_verified] = verify(*pk, msg.slice(), makeSlice(*sig));
return result;
}
} // namespace xrpl

View File

@@ -3,7 +3,7 @@
#include <xrpld/rpc/GRPCHandlers.h>
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
#include <xrpld/rpc/handlers/LedgerHandler.h>
#include <xrpld/rpc/handlers/ledger/Ledger.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>

View File

@@ -1,7 +1,7 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/GRPCHandlers.h>
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
#include <xrpld/rpc/handlers/LedgerEntryHelpers.h>
#include <xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/strHex.h>

View File

@@ -0,0 +1,21 @@
#include <xrpld/rpc/BookChanges.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
#include <xrpl/ledger/ReadView.h>
namespace xrpl {
Json::Value
doBookChanges(RPC::JsonContext& context)
{
std::shared_ptr<ReadView const> ledger;
Json::Value result = RPC::lookupLedger(ledger, context);
if (ledger == nullptr)
return result;
return RPC::computeBookChanges(ledger);
}
} // namespace xrpl

View File

@@ -1,5 +1,4 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/BookChanges.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
@@ -205,16 +204,4 @@ doBookOffers(RPC::JsonContext& context)
return jvResult;
}
Json::Value
doBookChanges(RPC::JsonContext& context)
{
std::shared_ptr<ReadView const> ledger;
Json::Value result = RPC::lookupLedger(ledger, context);
if (ledger == nullptr)
return result;
return RPC::computeBookChanges(ledger);
}
} // namespace xrpl

View File

@@ -0,0 +1,24 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/jss.h>
namespace xrpl {
Json::Value
doNFTBuyOffers(RPC::JsonContext& context)
{
if (!context.params.isMember(jss::nft_id))
return RPC::missing_field_error(jss::nft_id);
uint256 nftId;
if (!nftId.parseHex(context.params[jss::nft_id].asString()))
return RPC::invalid_field_error(jss::nft_id);
return enumerateNFTOffers(context, nftId, keylet::nft_buys(nftId));
}
} // namespace xrpl

View File

@@ -1,3 +1,5 @@
#pragma once
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/RPCLedgerHelpers.h>
@@ -14,7 +16,7 @@
namespace xrpl {
static void
inline void
appendNftOfferJson(
Application const& app,
std::shared_ptr<SLE const> const& offer,
@@ -42,7 +44,7 @@ appendNftOfferJson(
// limit: integer // optional
// marker: opaque // optional, resume previous query
// }
static Json::Value
inline Json::Value
enumerateNFTOffers(RPC::JsonContext& context, uint256 const& nftId, Keylet const& directory)
{
unsigned int limit = 0;
@@ -127,32 +129,4 @@ enumerateNFTOffers(RPC::JsonContext& context, uint256 const& nftId, Keylet const
return result;
}
Json::Value
doNFTSellOffers(RPC::JsonContext& context)
{
if (!context.params.isMember(jss::nft_id))
return RPC::missing_field_error(jss::nft_id);
uint256 nftId;
if (!nftId.parseHex(context.params[jss::nft_id].asString()))
return RPC::invalid_field_error(jss::nft_id);
return enumerateNFTOffers(context, nftId, keylet::nft_sells(nftId));
}
Json::Value
doNFTBuyOffers(RPC::JsonContext& context)
{
if (!context.params.isMember(jss::nft_id))
return RPC::missing_field_error(jss::nft_id);
uint256 nftId;
if (!nftId.parseHex(context.params[jss::nft_id].asString()))
return RPC::invalid_field_error(jss::nft_id);
return enumerateNFTOffers(context, nftId, keylet::nft_buys(nftId));
}
} // namespace xrpl

View File

@@ -0,0 +1,24 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/jss.h>
namespace xrpl {
Json::Value
doNFTSellOffers(RPC::JsonContext& context)
{
if (!context.params.isMember(jss::nft_id))
return RPC::missing_field_error(jss::nft_id);
uint256 nftId;
if (!nftId.parseHex(context.params[jss::nft_id].asString()))
return RPC::invalid_field_error(jss::nft_id);
return enumerateNFTOffers(context, nftId, keylet::nft_sells(nftId));
}
} // namespace xrpl

View File

@@ -190,8 +190,14 @@ populateJsonResponse(
auto const& sttx = result.txn->getSTransaction();
if (context.apiVersion > 1)
{
constexpr auto optionsJson =
// In API v2, include_date and disable_API_prior_V2 are used to
// include date/ledger_index/ctid in tx_json. In API v3+, those
// fields are excluded from tx_json and are only at result level.
constexpr auto optionsV2 =
JsonOptions::include_date | JsonOptions::disable_API_prior_V2;
constexpr auto optionsV3 =
JsonOptions::disable_API_prior_V2 | JsonOptions::disable_API_prior_V3;
auto const optionsJson = context.apiVersion >= 3 ? optionsV3 : optionsV2;
if (args.binary)
{
response[jss::tx_blob] = result.txn->getJson(optionsJson, true);
@@ -213,7 +219,11 @@ populateJsonResponse(
{
response[jss::ledger_index] = result.txn->getLedger();
if (result.closeTime)
{
response[jss::close_time_iso] = to_string_iso(*result.closeTime);
if (context.apiVersion >= 3)
response[jss::date] = result.closeTime->time_since_epoch().count();
}
}
}
else