mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-19 18:18:12 +00:00
Compare commits
1 Commits
bthomee/rp
...
bthomee/rp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a8b4849e |
@@ -30,6 +30,7 @@ This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-R
|
||||
- The command line client no longer prints a credential the operator did not type. A failing command echoes the request it built under `request_sent`, which carries the `admin_password` the client copies out of `[port_rpc]` in the config, so `./xrpld account_info rBogus` printed that password to stdout and into any captured output. `request_sent` is now masked. The `rpc` member beside it, which echoes the arguments as they were typed, is unchanged. The command line client also no longer writes an unparsed `json` or `ripple_path_find` argument to its trace log before parsing it, where a `secret` inside that argument could not be masked; it logs the parsed request instead, masked. The reply it receives is logged the same way, parsed and masked, where the raw body was written before, a `validation_create` answer included.
|
||||
- A WebSocket frame that does not parse, or exceeds the request size limit, is answered `{"type": "error", "error": "jsonInvalid", "size": <bytes>}`. The frame's body is reported by size rather than echoed back in a `value` member, since a body that does not parse has no fields to mask. A client that read `value` gets `size` instead.
|
||||
- Four error codes that named no HTTP status of their own, and so answered 200 on a reply reporting an error, now name one: `actMalformed`, `alreadyMultisig` and `alreadySingleSig` answer 400, and `actNotFound` answers 404. **No shipped envelope reports these four.** A request sending `ripplerpc: "3.0"` still receives 200 for all four, as it always has, so `account_info` on a malformed account or one the ledger does not hold answers 200 exactly as before.
|
||||
- `submit`, `simulate`, `transaction_entry`, `ledger_entry` and `ledger_accept`: Errors from these methods now include `error_code` and `error_message` alongside the `error` token, as every other method already did. Each error now answers the status its code names: 400 for a malformed request, 404 for `transactionNotFound`, 500 for an internal failure, and 501 for `notYetImplemented` and `notStandAlone`. That status change reaches only a request sending `ripplerpc: "3.0"`, which is the envelope that derives the status from the error. With `ripplerpc` `"1.0"` the status stays 200 and the two new members appear beside `error`; with `"2.0"` the status stays 200, `error_code` appears, and the `code` and `message` members carry the code and the message rather than null, since that envelope copies them from `error_code` and `error_message` and drops `error_message`.
|
||||
|
||||
## XRP Ledger server version 3.5.0
|
||||
|
||||
|
||||
@@ -144,7 +144,27 @@ enum ErrorCodeI {
|
||||
RpcEntryNotFound = 98,
|
||||
RpcUnexpectedLedgerType = 99,
|
||||
|
||||
RpcLast = RpcUnexpectedLedgerType, // rpcLAST should always equal the last code.
|
||||
// submit + simulate
|
||||
RpcInvalidTransaction = 100,
|
||||
RpcInternalSubmit = 101,
|
||||
RpcInternalJson = 102,
|
||||
RpcInternalSimulate = 103,
|
||||
|
||||
// transaction_entry
|
||||
RpcFieldNotFoundTransaction = 104,
|
||||
RpcNotYetImplemented = 105,
|
||||
RpcTransactionNotFound = 106,
|
||||
|
||||
// transaction_entry
|
||||
RpcMalformedRequest = 107,
|
||||
|
||||
// ledger_accept
|
||||
RpcNotStandAlone = 108,
|
||||
|
||||
// ledger_entry, API version 1 only
|
||||
RpcUnknownOption = 109,
|
||||
|
||||
RpcLast = RpcUnknownOption, // rpcLAST should always equal the last code.
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -245,7 +245,7 @@ JSS(ephemeral_key); // out: ValidatorInfo
|
||||
JSS(error); // out: error
|
||||
JSS(errored); //
|
||||
JSS(error_code); // out: error
|
||||
JSS(error_exception); // out: Submit
|
||||
JSS(error_exception); // out: Submit, Simulate
|
||||
JSS(error_message); // out: error
|
||||
JSS(expand); // in: handler/Ledger
|
||||
JSS(expected_date); // out: any (warnings)
|
||||
|
||||
@@ -106,6 +106,16 @@ static constexpr ErrorInfo kUnorderedErrorInfos[]{
|
||||
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
|
||||
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
|
||||
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
|
||||
{RpcInvalidTransaction, "invalidTransaction", "Transaction is invalid.", 400},
|
||||
{RpcInternalSubmit, "internalSubmit", "Internal error during submit.", 500},
|
||||
{RpcInternalJson, "internalJson", "Internal error during JSON handling.", 500},
|
||||
{RpcInternalSimulate, "internalSimulate", "Internal error during simulate.", 500},
|
||||
{RpcFieldNotFoundTransaction, "fieldNotFoundTransaction", "Missing required field.", 400},
|
||||
{RpcNotYetImplemented, "notYetImplemented", "Not yet implemented.", 501},
|
||||
{RpcTransactionNotFound, "transactionNotFound", "Transaction not found.", 404},
|
||||
{RpcMalformedRequest, "malformedRequest", "Request is malformed.", 400},
|
||||
{RpcNotStandAlone, "notStandAlone", "Server is not running stand-alone.", 501},
|
||||
{RpcUnknownOption, "unknownOption", "Unknown option.", 400},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -56,8 +56,10 @@
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <source_location>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -164,6 +166,68 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
jv[jss::error] == err,
|
||||
"Expected error " + err + ", received " + jv[jss::error].asString() + ", at line " +
|
||||
std::to_string(location.line()) + ", " + jv.toStyledString());
|
||||
|
||||
// Wire values pinned as literals: read from the error table, the expectation would
|
||||
// agree with any change to the row that produced the reply.
|
||||
// These are raised through injectError and carry their own code.
|
||||
static std::map<std::string, int> const kCodes{
|
||||
{"entryNotFound", 98},
|
||||
{"invalidParams", 31},
|
||||
{"lgrNotFound", 21},
|
||||
{"unexpectedLedgerType", 99},
|
||||
{"unknownOption", 109},
|
||||
};
|
||||
|
||||
// The tokens the ledger_entry helpers name, all reporting the generic invalidParams;
|
||||
// see LedgerEntryHelpers.h. Listed, so a misspelled token reaches the else below.
|
||||
static std::set<std::string> const kMalformedTokens{
|
||||
"malformedAccount",
|
||||
"malformedAddress",
|
||||
"malformedAuthorized",
|
||||
"malformedAuthorizedCredentials",
|
||||
"malformedBridgeAccount",
|
||||
"malformedBroker",
|
||||
"malformedCurrency",
|
||||
"malformedDirRoot",
|
||||
"malformedDocumentID",
|
||||
"malformedIssue",
|
||||
"malformedIssuingChainDoor",
|
||||
"malformedLockingChainDoor",
|
||||
"malformedMPTIssuanceID",
|
||||
"malformedMPTokenIssuance",
|
||||
"malformedOwner",
|
||||
"malformedRequest",
|
||||
"malformedSeq",
|
||||
"malformedSponsee",
|
||||
"malformedSponsor",
|
||||
"malformedXChainOwnedClaimID",
|
||||
"malformedXChainOwnedCreateAccountClaimID",
|
||||
};
|
||||
|
||||
auto const expectCode = [&](int expected) {
|
||||
BEAST_EXPECTS(
|
||||
jv[jss::error_code] == expected,
|
||||
"Expected error_code " + std::to_string(expected) + " for " + err +
|
||||
", received " + jv[jss::error_code].toStyledString() + ", at line " +
|
||||
std::to_string(location.line()));
|
||||
};
|
||||
|
||||
if (auto const it = kCodes.find(err); it != kCodes.end())
|
||||
{
|
||||
expectCode(it->second);
|
||||
}
|
||||
else if (kMalformedTokens.contains(err))
|
||||
{
|
||||
expectCode(RpcInvalidParams);
|
||||
}
|
||||
else
|
||||
{
|
||||
// A token in neither list is a typo; reporting 31 like the rest would let it pass.
|
||||
BEAST_EXPECTS(
|
||||
false,
|
||||
"Token " + err + " names no error code, at line " +
|
||||
std::to_string(location.line()));
|
||||
}
|
||||
}
|
||||
if (msg.empty())
|
||||
{
|
||||
@@ -340,7 +404,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result];
|
||||
if (apiVersion < 2u)
|
||||
{
|
||||
checkErrorValue(jrr, "unknownOption", "", location);
|
||||
checkErrorValue(jrr, "unknownOption", "Unknown option.", location);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -546,7 +610,7 @@ class LedgerEntry_test : public beast::unit_test::Suite
|
||||
|
||||
if (apiVersion < 2u)
|
||||
{
|
||||
checkErrorValue(jrr, "unknownOption", "");
|
||||
checkErrorValue(jrr, "unknownOption", "Unknown option.");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -328,6 +328,21 @@ class Simulate_test : public beast::unit_test::Suite
|
||||
resp[jss::result][jss::error].toStyledString());
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Account'.");
|
||||
}
|
||||
{
|
||||
// A non-string `Account`, which no earlier check rejects: the `tx_json` arm validates
|
||||
// only that it is an object, and the check above only that `Account` is present.
|
||||
json::Value params;
|
||||
json::Value txJson = json::ValueType::Object;
|
||||
txJson[jss::TransactionType] = jss::AccountSet;
|
||||
txJson[jss::Account] = 123;
|
||||
params[jss::tx_json] = txJson;
|
||||
|
||||
auto const resp = env.rpc("json", "simulate", to_string(params));
|
||||
BEAST_EXPECTS(
|
||||
resp[jss::result][jss::error] == "invalidParams",
|
||||
resp[jss::result][jss::error].toStyledString());
|
||||
BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Account'.");
|
||||
}
|
||||
{
|
||||
// Account doesn't exist for Sequence autofill
|
||||
json::Value params;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <memory>
|
||||
#include <source_location>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
@@ -25,6 +26,45 @@ namespace xrpl {
|
||||
|
||||
class TransactionEntry_test : public beast::unit_test::Suite
|
||||
{
|
||||
/**
|
||||
* Asserts the reply carries the whole error, not just the token.
|
||||
*
|
||||
* @param result The reply's payload.
|
||||
* @param token The `error` token expected.
|
||||
* @param code The `error_code` expected.
|
||||
* @param message The `error_message` expected.
|
||||
* @param location The caller, reported when an assertion fails.
|
||||
*/
|
||||
void
|
||||
checkCodedError(
|
||||
json::Value const& result,
|
||||
std::string const& token,
|
||||
ErrorCodeI code,
|
||||
std::string const& message,
|
||||
std::source_location const location = std::source_location::current())
|
||||
{
|
||||
auto const at = std::to_string(location.line());
|
||||
|
||||
BEAST_EXPECTS(result[jss::error] == token, at);
|
||||
BEAST_EXPECTS(result[jss::error_code] == code, at);
|
||||
BEAST_EXPECTS(result[jss::error_message] == message, at);
|
||||
BEAST_EXPECTS(result[jss::status] == "error", at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire values pinned as literals, so reassigning a code fails here.
|
||||
*/
|
||||
void
|
||||
testErrorCodeValues()
|
||||
{
|
||||
testcase("Error code values are stable");
|
||||
|
||||
BEAST_EXPECT(static_cast<int>(RpcFieldNotFoundTransaction) == 104);
|
||||
BEAST_EXPECT(static_cast<int>(RpcNotYetImplemented) == 105);
|
||||
BEAST_EXPECT(static_cast<int>(RpcTransactionNotFound) == 106);
|
||||
BEAST_EXPECT(static_cast<int>(RpcMalformedRequest) == 107);
|
||||
}
|
||||
|
||||
void
|
||||
testBadInput()
|
||||
{
|
||||
@@ -38,8 +78,12 @@ class TransactionEntry_test : public beast::unit_test::Suite
|
||||
{
|
||||
// no params
|
||||
auto const result = env.client().invoke("transaction_entry", {})[jss::result];
|
||||
BEAST_EXPECT(result[jss::error] == "fieldNotFoundTransaction");
|
||||
BEAST_EXPECT(result[jss::status] == "error");
|
||||
// The message names the missing field rather than the table's default.
|
||||
checkCodedError(
|
||||
result,
|
||||
"fieldNotFoundTransaction",
|
||||
RpcFieldNotFoundTransaction,
|
||||
"Missing field 'tx_hash'.");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -55,8 +99,8 @@ class TransactionEntry_test : public beast::unit_test::Suite
|
||||
params[jss::ledger] = "current";
|
||||
params[jss::tx_hash] = "DEADBEEF";
|
||||
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::error] == "notYetImplemented");
|
||||
BEAST_EXPECT(result[jss::status] == "error");
|
||||
checkCodedError(
|
||||
result, "notYetImplemented", RpcNotYetImplemented, "Not yet implemented.");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -65,8 +109,8 @@ class TransactionEntry_test : public beast::unit_test::Suite
|
||||
params[jss::tx_hash] =
|
||||
"E2FE8D4AF3FCC3944DDF6CD8CDDC5E3F0AD50863EF8919AFEF10CB6408CD4D05";
|
||||
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::error] == "notYetImplemented");
|
||||
BEAST_EXPECT(result[jss::status] == "error");
|
||||
checkCodedError(
|
||||
result, "notYetImplemented", RpcNotYetImplemented, "Not yet implemented.");
|
||||
BEAST_EXPECT(result.isMember(jss::ledger_current_index));
|
||||
BEAST_EXPECT(!result.isMember(jss::ledger_hash));
|
||||
BEAST_EXPECT(result[jss::validated] == false);
|
||||
@@ -78,18 +122,19 @@ class TransactionEntry_test : public beast::unit_test::Suite
|
||||
params[jss::tx_hash] = "DEADBEEF";
|
||||
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
|
||||
BEAST_EXPECT(!result[jss::ledger_hash].asString().empty());
|
||||
BEAST_EXPECT(result[jss::error] == "malformedRequest");
|
||||
BEAST_EXPECT(result[jss::status] == "error");
|
||||
checkCodedError(
|
||||
result, "malformedRequest", RpcMalformedRequest, "Request is malformed.");
|
||||
}
|
||||
|
||||
// A `tx_hash` that is not a string is malformed, the same as one that is not hex.
|
||||
for (auto const type : {json::ValueType::Object, json::ValueType::Array})
|
||||
{
|
||||
json::Value params{json::ValueType::Object};
|
||||
params[jss::ledger] = "closed";
|
||||
params[jss::tx_hash] = json::Value{type};
|
||||
auto const result = env.client().invoke("transaction_entry", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::error] == "malformedRequest");
|
||||
BEAST_EXPECT(result[jss::status] == "error");
|
||||
checkCodedError(
|
||||
result, "malformedRequest", RpcMalformedRequest, "Request is malformed.");
|
||||
}
|
||||
|
||||
std::string const txHash{
|
||||
@@ -150,8 +195,11 @@ class TransactionEntry_test : public beast::unit_test::Suite
|
||||
// Valid structure, but transaction not found.
|
||||
json::Value const result{env.rpc("transaction_entry", txHash, "closed")};
|
||||
BEAST_EXPECT(!result[jss::result][jss::ledger_hash].asString().empty());
|
||||
BEAST_EXPECT(result[jss::result][jss::error] == "transactionNotFound");
|
||||
BEAST_EXPECT(result[jss::result][jss::status] == "error");
|
||||
checkCodedError(
|
||||
result[jss::result],
|
||||
"transactionNotFound",
|
||||
RpcTransactionNotFound,
|
||||
"Transaction not found.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +427,7 @@ public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testErrorCodeValues();
|
||||
testBadInput();
|
||||
forAllApiVersions([this](unsigned apiVersion) { testRequest(apiVersion); });
|
||||
}
|
||||
|
||||
@@ -1233,6 +1233,213 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a request carrying the `ripplerpc` parameter, which selects the
|
||||
* reply envelope.
|
||||
*
|
||||
* @param method The method the request names.
|
||||
* @param version The `ripplerpc` value. Empty omits the field entirely.
|
||||
* @return The request, serialized.
|
||||
*/
|
||||
static std::string
|
||||
makeRippleRpcRequest(std::string_view method, std::string_view version)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::method] = method;
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
json::Value params(json::ValueType::Object);
|
||||
if (!version.empty())
|
||||
params[jss::ripplerpc] = version;
|
||||
jv[jss::params][0u] = params;
|
||||
return to_string(jv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Four of the five handlers that report a bare token carry a code and
|
||||
* message with it: `transaction_entry`, `ledger_entry`, `submit` and
|
||||
* `simulate`, with `vault_info` as the unchanged control. `ledger_accept`
|
||||
* reports `notStandAlone` only on a networked server that is synced,
|
||||
* which no test harness provides, so that arm is not exercised here.
|
||||
*
|
||||
* @param yield The coroutine the requests run on.
|
||||
*/
|
||||
void
|
||||
testHandlerErrorsCarryCodes(boost::asio::yield_context& yield)
|
||||
{
|
||||
testcase("Handler errors carry a code and message");
|
||||
|
||||
using namespace test::jtx;
|
||||
Env env{*this};
|
||||
|
||||
boost::system::error_code ec;
|
||||
|
||||
// Both `ripplerpc` envelopes need all three present: version 3 derives the HTTP status
|
||||
// from `error_code`, and version 2 copies the pair into `code` and `message`, so a missing
|
||||
// field leaves the reply claiming success or carrying an explicit null. Expected values are
|
||||
// literals, so a row changing under a client turns this red.
|
||||
struct Case
|
||||
{
|
||||
std::string_view method;
|
||||
std::string_view token;
|
||||
ErrorCodeI code;
|
||||
std::string_view message;
|
||||
unsigned status;
|
||||
};
|
||||
for (auto const& [method, token, code, message, httpStatus] : {
|
||||
Case{
|
||||
.method = "transaction_entry",
|
||||
.token = "fieldNotFoundTransaction",
|
||||
.code = RpcFieldNotFoundTransaction,
|
||||
.message = "Missing field 'tx_hash'.",
|
||||
.status = 400,
|
||||
},
|
||||
Case{
|
||||
.method = "vault_info",
|
||||
.token = "invalidParams",
|
||||
.code = RpcInvalidParams,
|
||||
.message = "Must specify either 'vault_id' or both 'owner' and 'seq'.",
|
||||
.status = 400,
|
||||
},
|
||||
// Reached only below API version 2; see the api_version case.
|
||||
Case{
|
||||
.method = "ledger_entry",
|
||||
.token = "unknownOption",
|
||||
.code = RpcUnknownOption,
|
||||
.message = "Unknown option.",
|
||||
.status = 400,
|
||||
},
|
||||
})
|
||||
{
|
||||
auto const status = static_cast<boost::beast::http::status>(httpStatus);
|
||||
auto const label = std::string{method};
|
||||
|
||||
// Version 2 must report a real code and message, never null.
|
||||
{
|
||||
Response resp;
|
||||
auto const reply = postAndParse(
|
||||
env,
|
||||
yield,
|
||||
resp,
|
||||
ec,
|
||||
makeRippleRpcRequest(method, rpc::kRippleRpcVersion2),
|
||||
label);
|
||||
auto const& error = reply[jss::error];
|
||||
BEAST_EXPECTS(error[jss::error] == token, label);
|
||||
BEAST_EXPECTS(error[jss::error_code] == code, label);
|
||||
BEAST_EXPECTS(error[jss::code] == code, label);
|
||||
BEAST_EXPECTS(error[jss::message] == message, label);
|
||||
// Version 2 always answers 200, whatever the error code.
|
||||
BEAST_EXPECTS(resp.result() == kOk, label);
|
||||
}
|
||||
|
||||
// Version 3 maps that code onto the HTTP status.
|
||||
{
|
||||
Response resp;
|
||||
doHTTPRequest(
|
||||
env,
|
||||
yield,
|
||||
false,
|
||||
resp,
|
||||
ec,
|
||||
makeRippleRpcRequest(method, rpc::kRippleRpcVersion3));
|
||||
BEAST_EXPECTS(resp.result() == status, label);
|
||||
}
|
||||
|
||||
// Version 1 keeps the token and gains a message.
|
||||
{
|
||||
Response resp;
|
||||
auto const reply = postAndParse(
|
||||
env,
|
||||
yield,
|
||||
resp,
|
||||
ec,
|
||||
makeRippleRpcRequest(method, rpc::kRippleRpcVersion1),
|
||||
label);
|
||||
auto const& result = reply[jss::result];
|
||||
BEAST_EXPECTS(result[jss::error] == token, label);
|
||||
BEAST_EXPECTS(result[jss::error_code] == code, label);
|
||||
BEAST_EXPECTS(result[jss::error_message] == message, label);
|
||||
}
|
||||
}
|
||||
|
||||
// `ledger_entry` reports `unknownOption` below API version 2 and `invalidParams` from
|
||||
// version 2 onwards. Both carry a code, so both select an HTTP status under ripplerpc 3.
|
||||
for (auto const apiVersion : {1u, 2u})
|
||||
{
|
||||
Response resp;
|
||||
json::Value jv;
|
||||
jv[jss::method] = "ledger_entry";
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
jv[jss::params][0u] = json::ValueType::Object;
|
||||
jv[jss::params][0u][jss::ripplerpc] = rpc::kRippleRpcVersion3;
|
||||
jv[jss::params][0u][jss::api_version] = apiVersion;
|
||||
|
||||
// Literals again, for the reason the case table above gives.
|
||||
auto const expected = apiVersion < 2 ? RpcUnknownOption : RpcInvalidParams;
|
||||
auto const token = apiVersion < 2 ? "unknownOption" : "invalidParams";
|
||||
|
||||
auto const label = std::to_string(apiVersion);
|
||||
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv), label);
|
||||
BEAST_EXPECTS(reply[jss::error][jss::error] == token, label);
|
||||
BEAST_EXPECTS(reply[jss::error][jss::error_code] == expected, label);
|
||||
BEAST_EXPECTS(resp.result() == kBadRequest, label);
|
||||
}
|
||||
|
||||
// `submit` reports `invalidTransaction` with a coded error, and adds `error_exception`
|
||||
// carrying the underlying failure detail.
|
||||
{
|
||||
Response resp;
|
||||
json::Value jv;
|
||||
jv[jss::method] = "submit";
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
jv[jss::params][0u] = json::ValueType::Object;
|
||||
jv[jss::params][0u][jss::tx_blob] = "DEADBEEF";
|
||||
jv[jss::params][0u][jss::ripplerpc] = rpc::kRippleRpcVersion3;
|
||||
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv));
|
||||
auto const& error = reply[jss::error];
|
||||
BEAST_EXPECT(error[jss::error] == "invalidTransaction");
|
||||
BEAST_EXPECT(error[jss::error_code] == RpcInvalidTransaction);
|
||||
BEAST_EXPECT(error[jss::message] == "Transaction is invalid.");
|
||||
BEAST_EXPECT(!error[jss::error_exception].asString().empty());
|
||||
BEAST_EXPECT(resp.result() == kBadRequest);
|
||||
}
|
||||
|
||||
// `simulate` reports `invalidTransaction` the same way when the transaction does not
|
||||
// build. A `tx_json` carrying a field the `AccountSet` template does not allow passes the
|
||||
// JSON parse and throws in the `STTx` constructor, the first `catch` in `doSimulate`.
|
||||
// Version 3 derives the status from the code; version 2 copies the pair.
|
||||
for (auto const version : {rpc::kRippleRpcVersion3, rpc::kRippleRpcVersion2})
|
||||
{
|
||||
Response resp;
|
||||
json::Value jv;
|
||||
jv[jss::method] = "simulate";
|
||||
jv[jss::params] = json::ValueType::Array;
|
||||
jv[jss::params][0u] = json::ValueType::Object;
|
||||
jv[jss::params][0u][jss::ripplerpc] = version;
|
||||
auto& txJson = jv[jss::params][0u][jss::tx_json];
|
||||
txJson[jss::TransactionType] = "AccountSet";
|
||||
txJson[jss::Account] = env.master.human();
|
||||
txJson[jss::Destination] = env.master.human();
|
||||
|
||||
auto const label = std::string{version};
|
||||
auto const reply = postAndParse(env, yield, resp, ec, to_string(jv), label);
|
||||
auto const& error = reply[jss::error];
|
||||
BEAST_EXPECTS(error[jss::error] == "invalidTransaction", label);
|
||||
BEAST_EXPECTS(error[jss::error_code] == RpcInvalidTransaction, label);
|
||||
BEAST_EXPECTS(error[jss::message] == "Transaction is invalid.", label);
|
||||
BEAST_EXPECTS(!error[jss::error_exception].asString().empty(), label);
|
||||
if (version == rpc::kRippleRpcVersion3)
|
||||
{
|
||||
BEAST_EXPECTS(resp.result() == kBadRequest, label);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(error[jss::code] == RpcInvalidTransaction, label);
|
||||
BEAST_EXPECTS(resp.result() == kOk, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `ripplerpc: "3.0"` envelope reports 200 for the codes below.
|
||||
*
|
||||
@@ -1848,6 +2055,7 @@ public:
|
||||
testTheLoggedReplyIsMaskedOnlyWhenItCarriesACredential(yield);
|
||||
testInternalErrorIsReportedOnBothTransports(yield);
|
||||
testNoCredentialReachesTheLogAtTrace(yield);
|
||||
testHandlerErrorsCarryCodes(yield);
|
||||
testGainedStatusesStayOffLegacyEnvelope(yield);
|
||||
testStatusNotOkay(yield);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpld/rpc/Context.h>
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/server/NetworkOPs.h>
|
||||
|
||||
@@ -11,6 +12,13 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Closes the open ledger on a stand-alone server.
|
||||
*
|
||||
* @param context The request. Its `params` are not read.
|
||||
* @return `ledger_current_index` of the ledger the close opens, or
|
||||
* `notStandAlone` when the server is on a network.
|
||||
*/
|
||||
json::Value
|
||||
doLedgerAccept(rpc::JsonContext& context)
|
||||
{
|
||||
@@ -18,7 +26,7 @@ doLedgerAccept(rpc::JsonContext& context)
|
||||
|
||||
if (!context.app.config().standalone())
|
||||
{
|
||||
jvResult[jss::error] = "notStandAlone";
|
||||
rpc::injectError(RpcNotStandAlone, jvResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1042,7 +1042,7 @@ doLedgerEntry(rpc::JsonContext& context)
|
||||
{
|
||||
if (context.apiVersion < 2u)
|
||||
{
|
||||
jvResult[jss::error] = "unknownOption";
|
||||
rpc::injectError(RpcUnknownOption, jvResult);
|
||||
return jvResult;
|
||||
}
|
||||
return rpc::makeParamError("No ledger_entry params provided.");
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
|
||||
namespace xrpl::ledger_entry_helpers {
|
||||
|
||||
// These helpers name the malformed field in the `error` token and report `invalidParams` as the
|
||||
// code, whatever the token is. A client has read 31 for every one of these tokens for years, so the
|
||||
// code is deliberately not derived from the token here.
|
||||
inline std::unexpected<json::Value>
|
||||
missingFieldError(json::StaticString const field, std::optional<std::string> err = std::nullopt)
|
||||
{
|
||||
|
||||
@@ -42,6 +42,17 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Picks the `Sequence` a simulated transaction runs with.
|
||||
*
|
||||
* @param txJson The transaction, which carries an `Account` member.
|
||||
* @param context The request, read for the open ledger and the queue.
|
||||
* @return 0 when the transaction names a `TicketSequence`, otherwise the
|
||||
* account's next queueable sequence. On either path, the error to
|
||||
* report when `Account` is not a string or does not parse as an
|
||||
* account; without a `TicketSequence`, also when the account is not
|
||||
* in the open ledger.
|
||||
*/
|
||||
static std::expected<std::uint32_t, json::Value>
|
||||
getAutofillSequence(json::Value const& txJson, rpc::JsonContext& context)
|
||||
{
|
||||
@@ -50,10 +61,10 @@ getAutofillSequence(json::Value const& txJson, rpc::JsonContext& context)
|
||||
auto const& accountStr = txJson[jss::Account];
|
||||
if (!accountStr.isString())
|
||||
{
|
||||
// sanity check, should fail earlier
|
||||
// LCOV_EXCL_START
|
||||
// The earlier check requires `Account` to be present, not to be a string, so this is the
|
||||
// type check. Without it `asString` below throws on a numeric value, which degrades a clean
|
||||
// field error into an internal one.
|
||||
return std::unexpected(rpc::invalidFieldError("tx.Account"));
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
auto const srcAddressID = parseBase58<AccountID>(accountStr.asString());
|
||||
if (!srcAddressID.has_value())
|
||||
@@ -240,6 +251,19 @@ getTxJsonFromParams(json::Value const& params)
|
||||
return txJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies @p transaction to a copy of the open ledger as a dry run.
|
||||
*
|
||||
* @param context The request, read for `binary`.
|
||||
* @param transaction The transaction to apply.
|
||||
* @return The engine result with `applied` and `ledger_index`, and the
|
||||
* transaction as `tx_blob` when `binary` is true, otherwise as
|
||||
* `tx_json`. Its metadata follows as `meta_blob` or `meta` only
|
||||
* when the engine produced any, which it does when the result is
|
||||
* `tesSUCCESS` or a `tec` code, since both apply the transaction
|
||||
* to the view. A `tel`, `tem`, `tef` or `ter` result applies
|
||||
* nothing and carries no metadata.
|
||||
*/
|
||||
static json::Value
|
||||
simulateTxn(rpc::JsonContext& context, std::shared_ptr<Transaction> transaction)
|
||||
{
|
||||
@@ -266,12 +290,11 @@ simulateTxn(rpc::JsonContext& context, std::shared_ptr<Transaction> transaction)
|
||||
}
|
||||
else
|
||||
{
|
||||
// shouldn't be hit
|
||||
// LCOV_EXCL_START
|
||||
// Every TER this can hold names a token, so this arm states the fallback rather than a
|
||||
// result any transaction reaches.
|
||||
jvResult[jss::engine_result] = "unknown";
|
||||
jvResult[jss::engine_result_code] = result.ter;
|
||||
jvResult[jss::engine_result_message] = "unknown";
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (token == "tesSUCCESS")
|
||||
@@ -307,10 +330,20 @@ simulateTxn(rpc::JsonContext& context, std::shared_ptr<Transaction> transaction)
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
// {
|
||||
// tx_blob: <string> XOR tx_json: <object>,
|
||||
// binary: <bool>
|
||||
// }
|
||||
/**
|
||||
* Dry-runs a transaction against the open ledger without submitting it.
|
||||
*
|
||||
* `params` carry `tx_blob` or `tx_json`, one of the two, and optionally
|
||||
* `binary`. A credential in `params` is refused. A `tx_json` missing
|
||||
* `Sequence`, `Fee` or `SigningPubKey` has it filled in, and one missing
|
||||
* `NetworkID` has it filled in where the network's ID is above 1024.
|
||||
*
|
||||
* @param context The request.
|
||||
* @return The result `simulateTxn` builds, or an error object:
|
||||
* `invalidTransaction` with `error_exception` when the transaction
|
||||
* does not build, `internalSimulate` with `error_exception` when
|
||||
* applying it throws.
|
||||
*/
|
||||
json::Value
|
||||
doSimulate(rpc::JsonContext& context)
|
||||
{
|
||||
@@ -353,7 +386,7 @@ doSimulate(rpc::JsonContext& context)
|
||||
catch (std::exception& e)
|
||||
{
|
||||
json::Value jvResult = json::ValueType::Object;
|
||||
jvResult[jss::error] = "invalidTransaction";
|
||||
rpc::injectError(RpcInvalidTransaction, jvResult);
|
||||
jvResult[jss::error_exception] = e.what();
|
||||
return jvResult;
|
||||
}
|
||||
@@ -377,15 +410,13 @@ doSimulate(rpc::JsonContext& context)
|
||||
{
|
||||
return simulateTxn(context, transaction);
|
||||
}
|
||||
// LCOV_EXCL_START this is just in case, so xrpld doesn't crash
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
json::Value jvResult = json::ValueType::Object;
|
||||
jvResult[jss::error] = "internalSimulate";
|
||||
rpc::injectError(RpcInternalSimulate, jvResult);
|
||||
jvResult[jss::error_exception] = e.what();
|
||||
return jvResult;
|
||||
}
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -37,10 +37,21 @@ getFailHard(rpc::JsonContext const& context)
|
||||
context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool());
|
||||
}
|
||||
|
||||
// {
|
||||
// tx_blob: <string> XOR tx_json: <object>,
|
||||
// secret: <secret>
|
||||
// }
|
||||
/**
|
||||
* Submits a transaction to the network.
|
||||
*
|
||||
* `params` carry `tx_blob`, a signed transaction, or `tx_json` with a
|
||||
* signing credential, which the server signs first when it allows signing;
|
||||
* that path is deprecated. `fail_hard` keeps a transaction the local checks
|
||||
* fail from being relayed.
|
||||
*
|
||||
* @param context The request.
|
||||
* @return The engine result and the transaction, or an error object:
|
||||
* `invalidTransaction` with `error_exception` when the blob does not
|
||||
* decode or fails the local checks, `internalSubmit` with
|
||||
* `error_exception` when processing throws, `internalJson` when
|
||||
* building the reply throws.
|
||||
*/
|
||||
json::Value
|
||||
doSubmit(rpc::JsonContext& context)
|
||||
{
|
||||
@@ -90,7 +101,7 @@ doSubmit(rpc::JsonContext& context)
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
jvResult[jss::error] = "invalidTransaction";
|
||||
rpc::injectError(RpcInvalidTransaction, jvResult);
|
||||
jvResult[jss::error_exception] = e.what();
|
||||
|
||||
return jvResult;
|
||||
@@ -106,7 +117,7 @@ doSubmit(rpc::JsonContext& context)
|
||||
context.app.getHashRouter(), *stTx, context.ledgerMaster.getCurrentLedger()->rules());
|
||||
if (validity != Validity::Valid)
|
||||
{
|
||||
jvResult[jss::error] = "invalidTransaction";
|
||||
rpc::injectError(RpcInvalidTransaction, jvResult);
|
||||
jvResult[jss::error_exception] = "fails local checks: " + reason;
|
||||
|
||||
return jvResult;
|
||||
@@ -117,7 +128,7 @@ doSubmit(rpc::JsonContext& context)
|
||||
auto transaction = std::make_shared<Transaction>(stTx, reason, context.app);
|
||||
if (transaction->getStatus() != TransStatus::NEW)
|
||||
{
|
||||
jvResult[jss::error] = "invalidTransaction";
|
||||
rpc::injectError(RpcInvalidTransaction, jvResult);
|
||||
jvResult[jss::error_exception] = "fails local checks: " + reason;
|
||||
|
||||
return jvResult;
|
||||
@@ -133,7 +144,7 @@ doSubmit(rpc::JsonContext& context)
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
jvResult[jss::error] = "internalSubmit";
|
||||
rpc::injectError(RpcInternalSubmit, jvResult);
|
||||
jvResult[jss::error_exception] = e.what();
|
||||
|
||||
return jvResult;
|
||||
@@ -179,7 +190,7 @@ doSubmit(rpc::JsonContext& context)
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
jvResult[jss::error] = "internalJson";
|
||||
rpc::injectError(RpcInternalJson, jvResult);
|
||||
jvResult[jss::error_exception] = e.what();
|
||||
|
||||
return jvResult;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <rpcspec/Errors.hpp>
|
||||
@@ -37,22 +38,23 @@ TransactionEntryHandler::process(Input const& input) const
|
||||
if (!input.txHash &&
|
||||
input.txHash.error() == ::rpc::spec::handlers::transaction_entry::TxHashError::Missing)
|
||||
{
|
||||
output.error = "fieldNotFoundTransaction";
|
||||
output.error = ::rpc::Status{
|
||||
RpcFieldNotFoundTransaction, missingFieldMessage(std::string{jss::tx_hash.cStr()})};
|
||||
}
|
||||
else if (output.ledger->open())
|
||||
{
|
||||
// We don't work on ledger current.
|
||||
output.error = "notYetImplemented";
|
||||
output.error = ::rpc::Status{RpcNotYetImplemented};
|
||||
}
|
||||
else if (!input.txHash)
|
||||
{
|
||||
output.error = "malformedRequest";
|
||||
output.error = ::rpc::Status{RpcMalformedRequest};
|
||||
}
|
||||
else
|
||||
{
|
||||
std::tie(output.tx, output.meta) = output.ledger->txRead(*input.txHash);
|
||||
if (!output.tx)
|
||||
output.error = "transactionNotFound";
|
||||
output.error = ::rpc::Status{RpcTransactionNotFound};
|
||||
}
|
||||
|
||||
return output;
|
||||
@@ -68,7 +70,9 @@ TransactionEntryHandler::writeResult(json::Value& value, Output const& output) c
|
||||
|
||||
if (output.error)
|
||||
{
|
||||
value[jss::error] = std::string{*output.error};
|
||||
// The error travels in the Output, not as a failed process(), so the reply keeps the
|
||||
// ledger fields written above beside the token, code and message.
|
||||
injectSpecError(value, *output.error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
@@ -25,16 +24,38 @@ public:
|
||||
struct Output
|
||||
{
|
||||
std::shared_ptr<ReadView const> ledger;
|
||||
std::optional<std::string_view> error;
|
||||
/**
|
||||
* The error reported beside the ledger fields when the lookup fails.
|
||||
*/
|
||||
std::optional<::rpc::Status> error;
|
||||
std::shared_ptr<STTx const> tx;
|
||||
std::shared_ptr<STObject const> meta;
|
||||
};
|
||||
|
||||
explicit TransactionEntryHandler(JsonContext&);
|
||||
|
||||
/**
|
||||
* Looks the transaction up in the selected ledger.
|
||||
*
|
||||
* @param input The parsed request: the ledger selected, and the `tx_hash`
|
||||
* read or the reason there is none.
|
||||
* @return The ledger with the transaction and its metadata, or the ledger
|
||||
* with an error carrying its code: `fieldNotFoundTransaction`
|
||||
* without a `tx_hash`, `notYetImplemented` when the ledger is the
|
||||
* open one, `malformedRequest` when `tx_hash` is not a hex string,
|
||||
* `transactionNotFound` when the ledger does not hold it. A ledger
|
||||
* that cannot be selected is returned as the unexpected Status.
|
||||
*/
|
||||
[[nodiscard]] std::expected<Output, ::rpc::Status>
|
||||
process(Input const& input) const;
|
||||
|
||||
/**
|
||||
* Writes the reply: the ledger fields, then either the error with its
|
||||
* code and message, or the transaction and its metadata.
|
||||
*
|
||||
* @param value The reply object written into.
|
||||
* @param output The result of `process`.
|
||||
*/
|
||||
void
|
||||
writeResult(json::Value& value, Output const& output) const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user