Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions API-CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ API version 2 is available in `rippled` version 2.0.0 and later. See [API-VERSIO

This version is supported by all `rippled` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.

## Unreleased

### Additions and bugfixes in unreleased version

- `noripple_check`: The `transactions` field is no longer included in the response if the response is an error or if there are no `problems`.
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changelog entry says transactions is omitted when there are no problems, but the handler still includes transactions: [] whenever transactions=true (even if problems is empty). Update the changelog to match the implemented behavior (or adjust the implementation if the changelog is the intended contract).

Suggested change
- `noripple_check`: The `transactions` field is no longer included in the response if the response is an error or if there are no `problems`.
- `noripple_check`: The `transactions` field is no longer included in the response if the response is an error. When `transactions=true` and there are no `problems`, the response includes `transactions: []`.

Copilot uses AI. Check for mistakes.

## XRP Ledger server version 3.1.0

[Version 3.1.0](https://github.com/XRPLF/rippled/releases/tag/3.1.0) was released on Jan 27, 2026.
Expand Down
3 changes: 3 additions & 0 deletions include/xrpl/protocol/jss.h
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ JSS(frozen_balances); // out: GatewayBalances
JSS(full); // in: LedgerClearer, handlers/Ledger
JSS(full_reply); // out: PathFind
JSS(fullbelow_size); // out: GetCounts
JSS(gateway); // in: noripple_check
JSS(git); // out: server_info
JSS(good); // out: RPCVersion
JSS(hash); // out: NetworkOPs, InboundLedger,
Expand Down Expand Up @@ -484,6 +485,7 @@ JSS(ports); // out: NetworkOPs
JSS(previous); // out: Reservations
JSS(previous_ledger); // out: LedgerPropose
JSS(price); // out: amm_info, AuctionSlot
JSS(problems); // out: noripple_check
JSS(proof); // in: BookOffers
JSS(propose_seq); // out: LedgerPropose
JSS(proposers); // out: NetworkOPs, LedgerConsensus
Expand Down Expand Up @@ -661,6 +663,7 @@ JSS(url); // in/out: Subscribe, Unsubscribe
JSS(url_password); // in: Subscribe
JSS(url_username); // in: Subscribe
JSS(urlgravatar); //
JSS(user); // in: noripple_check
JSS(username); // in: Subscribe
JSS(validated); // out: NetworkOPs, RPCHelpers, AccountTx*
// Tx
Expand Down
8 changes: 6 additions & 2 deletions src/test/rpc/NoRippleCheck_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,16 @@ class NoRippleCheck_test : public beast::unit_test::suite

{ // passing an account private key will cause
// parsing as a seed to fail
// also, `transactions` is not included
Json::Value params;
params[jss::account] = toBase58(TokenType::NodePrivate, alice.sk());
params[jss::role] = "user";
params[jss::ledger] = "current";
params[jss::transactions] = true;
auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
BEAST_EXPECT(result[jss::error] == "actMalformed");
BEAST_EXPECT(result[jss::error_message] == "Account malformed.");
BEAST_EXPECT(!result.isMember(jss::transactions));
}

{
Expand Down Expand Up @@ -173,6 +176,7 @@ class NoRippleCheck_test : public beast::unit_test::suite
if (!BEAST_EXPECT(pa.isArray()))
return;

BEAST_EXPECT(!result.isMember(jss::transactions));
if (problems)
{
if (!BEAST_EXPECT(pa.size() == 2))
Expand All @@ -198,12 +202,12 @@ class NoRippleCheck_test : public beast::unit_test::suite
// time.
params[jss::transactions] = true;
result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
if (!BEAST_EXPECT(result[jss::transactions].isArray()))
return;

auto const txs = result[jss::transactions];
if (problems)
{
if (!BEAST_EXPECT(result[jss::transactions].isArray()))
return;
Comment on lines +212 to +213
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API changelog states that the transactions field should not be included when there are no problems. However, the test expectation on line 228 checks txs.size() == 0, which implicitly expects an empty array rather than the absence of the field. This test should be updated to check !result.isMember(jss::transactions) when there are no problems, even if the transactions parameter was set to true. Note that line 206 also unsafely accesses result[jss::transactions] without checking if the field exists first - this should be guarded with a membership check.

Copilot uses AI. Check for mistakes.
if (!BEAST_EXPECT(txs.size() == (user ? 1 : 2)))
return;

Expand Down
96 changes: 53 additions & 43 deletions src/xrpld/rpc/handlers/NoRippleCheck.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ fillTransaction(
std::uint32_t& sequence,
ReadView const& ledger)
{
txArray["Sequence"] = Json::UInt(sequence++);
txArray["Account"] = toBase58(accountID);
txArray[jss::Sequence] = Json::UInt(sequence++);
txArray[jss::Account] = toBase58(accountID);
auto& fees = ledger.fees();
// Convert the reference transaction cost in fee units to drops
// scaled to represent the current fee load.
txArray["Fee"] = scaleFeeLoad(fees.base, context.app.getFeeTrack(), fees, false).jsonClipped();
txArray[jss::Fee] = scaleFeeLoad(fees.base, context.app.getFeeTrack(), fees, false).jsonClipped();
}

// {
Expand All @@ -42,32 +42,40 @@ Json::Value
doNoRippleCheck(RPC::JsonContext& context)
{
auto const& params(context.params);
if (!params.isMember(jss::account))
return RPC::missing_field_error("account");

if (!params.isMember("role"))
return RPC::missing_field_error("role");
// check account param
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);
}
Comment on lines +54 to +58
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the parseBase58 failure to return rpcError(rpcACT_MALFORMED) before lookupLedger changes the error response shape beyond omitting transactions (it will also no longer include ledger metadata like ledger_hash/ledger_index/validated that previously came from lookupLedger). Please confirm this behavior change is intended and, if so, document it in the API changelog/PR description.

Copilot uses AI. Check for mistakes.
auto const accountID{std::move(id.value())};

// check role param
if (!params.isMember(jss::role))
return RPC::missing_field_error(jss::role);

bool roleGateway = false;
{
std::string const role = params["role"].asString();
if (role == "gateway")
std::string const role = params[jss::role].asString();
if (role == jss::gateway)
roleGateway = true;
else if (role != "user")
return RPC::invalid_field_error("role");
else if (role != jss::user)
return RPC::invalid_field_error(jss::role);
Comment on lines +62 to +71
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

role is treated as a string without validating its JSON type. If role is non-string (e.g. object/number), asString() will silently coerce and may accept invalid input. Add an isString() check for params[jss::role] and return RPC::invalid_field_error(jss::role) when it is not a string.

Copilot uses AI. Check for mistakes.
}

// check limit param
unsigned int limit;
if (auto err = readLimitField(limit, RPC::Tuning::noRippleCheck, context))
return *err;

bool transactions = false;
if (params.isMember(jss::transactions))
transactions = params["transactions"].asBool();

// check transactions param
// The document[https://xrpl.org/noripple_check.html#noripple_check] states
// that transactions params is a boolean value, however, assigning any
// string value works. Do not allow this. This check is for api Version 2
Expand All @@ -77,93 +85,95 @@ doNoRippleCheck(RPC::JsonContext& context)
return RPC::invalid_field_error(jss::transactions);
}

bool transactions = false;
if (params.isMember(jss::transactions))
transactions = params[jss::transactions].asBool();

// lookup ledger via params
std::shared_ptr<ReadView const> ledger;
auto result = RPC::lookupLedger(ledger, context);
if (!ledger)
return result;

Json::Value dummy;
Json::Value& jvTransactions = transactions ? (result[jss::transactions] = Json::arrayValue) : dummy;

auto id = parseBase58<AccountID>(params[jss::account].asString());
if (!id)
{
RPC::inject_error(rpcACT_MALFORMED, result);
return result;
}
auto const accountID{std::move(id.value())};
auto const sle = ledger->read(keylet::account(accountID));
if (!sle)
return rpcError(rpcACT_NOT_FOUND);

std::uint32_t seq = sle->getFieldU32(sfSequence);

Json::Value& problems = (result["problems"] = Json::arrayValue);
Json::Value& problems = (result[jss::problems] = Json::arrayValue);

bool defaultRipple = sle->getFieldU32(sfFlags) & lsfDefaultRipple;

bool bDefaultRipple = sle->getFieldU32(sfFlags) & lsfDefaultRipple;
Json::Value jvTransactions = Json::arrayValue;

if (bDefaultRipple & !roleGateway)
if (defaultRipple & !roleGateway)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses bitwise AND (&) instead of logical AND (&&). Since defaultRipple is a boolean, this should use && to avoid unexpected behavior. The same issue appears on line 117.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These conditions use bitwise & with boolean operands. While it works, it removes short-circuiting and is inconsistent with nearby boolean logic (&&). Use && here to make intent clear and avoid subtle precedence/maintenance issues.

Copilot uses AI. Check for mistakes.
{
Comment on lines +112 to 113
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultRipple and roleGateway are booleans; using bitwise & here avoids short-circuiting and is easy to misread as a bug. Use logical && for boolean conditions.

Copilot uses AI. Check for mistakes.
problems.append(
"You appear to have set your default ripple flag even though you "
"are not a gateway. This is not recommended unless you are "
"experimenting");
}
else if (roleGateway & !bDefaultRipple)
else if (roleGateway & !defaultRipple)
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect operator used: bitwise AND (&) is used instead of logical AND (&&). The expression roleGateway & !defaultRipple performs bitwise AND between two boolean values, which can lead to incorrect behavior. This should be roleGateway && !defaultRipple to perform logical AND.

Copilot uses AI. Check for mistakes.
{
Comment on lines +119 to 120
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition also uses bitwise & with boolean operands. Prefer && for boolean logic to match the surrounding code and avoid surprises from lack of short-circuiting.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to 120
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These conditions use bitwise & with boolean operands (defaultRipple/roleGateway). While it currently works, it does not short-circuit and is easy to misread as a bug; other handlers in this codebase generally use logical &&/|| for boolean logic. Replace & with && here (and similarly for the else if).

Copilot uses AI. Check for mistakes.
Comment on lines +112 to 120
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These conditions use bitwise & with boolean operands (defaultRipple & !roleGateway, roleGateway & !defaultRipple). This works but is easy to misread and does not short-circuit; it also regresses the earlier change in this PR thread. Use logical && for boolean logic here.

Copilot uses AI. Check for mistakes.
problems.append("You should immediately set your default ripple flag");
if (transactions)
{
Json::Value& tx = jvTransactions.append(Json::objectValue);
tx["TransactionType"] = jss::AccountSet;
tx["SetFlag"] = 8;
tx[jss::TransactionType] = jss::AccountSet;
tx[jss::SetFlag] = 8;
fillTransaction(context, tx, accountID, seq, *ledger);
}
}

forEachItemAfter(*ledger, accountID, uint256(), 0, limit, [&](std::shared_ptr<SLE const> const& ownedItem) {
if (ownedItem->getType() == ltRIPPLE_STATE)
{
bool const bLow = accountID == ownedItem->getFieldAmount(sfLowLimit).getIssuer();
bool const low = accountID == ownedItem->getFieldAmount(sfLowLimit).getIssuer();

bool const bNoRipple = ownedItem->getFieldU32(sfFlags) & (bLow ? lsfLowNoRipple : lsfHighNoRipple);
bool const noRipple = ownedItem->getFieldU32(sfFlags) & (low ? lsfLowNoRipple : lsfHighNoRipple);

std::string problem;
bool needFix = false;
if (bNoRipple & roleGateway)
if (noRipple && roleGateway)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This correctly uses logical AND (&&), but line 110 and 117 in the same function use bitwise AND (&) for the same type of boolean comparison. For consistency and correctness, all boolean comparisons should use logical operators.

Copilot uses AI. Check for mistakes.
{
problem = "You should clear the no ripple flag on your ";
needFix = true;
}
else if (!roleGateway & !bNoRipple)
else if (!roleGateway && !noRipple)
{
problem = "You should probably set the no ripple flag on your ";
needFix = true;
}
if (needFix)
{
AccountID peer = ownedItem->getFieldAmount(bLow ? sfHighLimit : sfLowLimit).getIssuer();
STAmount peerLimit = ownedItem->getFieldAmount(bLow ? sfHighLimit : sfLowLimit);
AccountID peer = ownedItem->getFieldAmount(low ? sfHighLimit : sfLowLimit).getIssuer();
STAmount peerLimit = ownedItem->getFieldAmount(low ? sfHighLimit : sfLowLimit);
problem += to_string(peerLimit.getCurrency());
problem += " line to ";
problem += to_string(peerLimit.getIssuer());
problems.append(problem);

STAmount limitAmount(ownedItem->getFieldAmount(bLow ? sfLowLimit : sfHighLimit));
STAmount limitAmount(ownedItem->getFieldAmount(low ? sfLowLimit : sfHighLimit));
limitAmount.setIssuer(peer);

Json::Value& tx = jvTransactions.append(Json::objectValue);
tx["TransactionType"] = jss::TrustSet;
tx["LimitAmount"] = limitAmount.getJson(JsonOptions::none);
tx["Flags"] = bNoRipple ? tfClearNoRipple : tfSetNoRipple;
fillTransaction(context, tx, accountID, seq, *ledger);
if (transactions)
{
Json::Value& tx = jvTransactions.append(Json::objectValue);
tx[jss::TransactionType] = jss::TrustSet;
tx[jss::LimitAmount] = limitAmount.getJson(JsonOptions::none);
tx[jss::Flags] = noRipple ? tfClearNoRipple : tfSetNoRipple;
fillTransaction(context, tx, accountID, seq, *ledger);
}

return true;
}
}
return false;
});

if (transactions)
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the API-CHANGELOG.md, the transactions field should not be included in the response if there are no problems. However, this code only checks if the transactions parameter is true, not whether there are any problems. This means an empty transactions array will still be returned when transactions=true but there are no problems, which contradicts the documented behavior. The condition should check both that transactions is true AND that jvTransactions is not empty, or check if problems.size() is greater than 0.

Suggested change
if (transactions)
if (transactions && !jvTransactions.empty())

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional

result[jss::transactions] = std::move(jvTransactions);
return result;
}

Expand Down
Loading