mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-21 19:18:22 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a8b4849e | ||
|
|
49410ad867 | ||
|
|
3689dca688 | ||
|
|
671a00b1ac | ||
|
|
9096ec03a7 | ||
|
|
5fec493960 | ||
|
|
a6a4fff2aa | ||
|
|
c2478f509c | ||
|
|
c908ed7839 | ||
|
|
fc57311c81 |
No files matched your search
@@ -174,6 +174,7 @@ test.server > xrpl.basics
|
||||
test.server > xrpl.config
|
||||
test.server > xrpld.app
|
||||
test.server > xrpld.core
|
||||
test.server > xrpld.rpc
|
||||
test.server > xrpl.json
|
||||
test.server > xrpl.protocol
|
||||
test.server > xrpl.server
|
||||
|
||||
@@ -22,6 +22,16 @@ API version 2 is available in `xrpld` version 2.0.0 and later. See [API-VERSION-
|
||||
|
||||
This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-RPC requests, it is currently the default API version used when no `api_version` is specified.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- A request echoed back in an error reply now has every credential-bearing field masked: `admin_password`, `admin_user`, `passphrase`, `password`, `secret`, `seed`, `seed_hex`, `url_password`, `url_username` and `username`. Nesting no longer matters, so a credential inside `params` is masked too. The same masking is applied to every request and reply written to the log, and it covers six further names that only a reply carries: `master_key`, `master_seed`, `master_seed_hex`, `validation_key`, `validation_private_key` and `validation_seed`, which is how `wallet_propose` and `validation_create` used to write a live private key to the log. A request or reply written to the log is truncated at 10,000 characters. An `[rpc_startup]` line that does not parse is reported by its position in the section and the parser's message, never by its text, and no command runs for it.
|
||||
- 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
|
||||
|
||||
Version 3.5.0 is not yet released.
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
|
||||
#include <concepts>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
@@ -198,6 +200,15 @@ public:
|
||||
*/
|
||||
Value(StaticString const& value);
|
||||
Value(std::string const& value);
|
||||
/**
|
||||
* @brief Constructs a value from a string view.
|
||||
*
|
||||
* The characters are copied, so the view need not outlive the call and need
|
||||
* not be NUL-terminated.
|
||||
*
|
||||
* @param value The characters to copy.
|
||||
*/
|
||||
Value(std::string_view value);
|
||||
Value(bool value);
|
||||
Value(Value const& other);
|
||||
~Value();
|
||||
@@ -472,6 +483,32 @@ toJson(xrpl::Number const& number)
|
||||
bool
|
||||
operator==(Value const&, Value const&);
|
||||
|
||||
/**
|
||||
* Compares a value with a string view, reading the value's characters in place
|
||||
* rather than building a Value from the view.
|
||||
*
|
||||
* Constrained to the exact type: a string literal converts equally well to a
|
||||
* view and to a Value, so a plain overload makes `value == "literal"`
|
||||
* ambiguous.
|
||||
*
|
||||
* @param x The value to compare.
|
||||
* @param y The characters to compare it against.
|
||||
* @return Whether `x` is a string whose characters up to its first NUL are
|
||||
* exactly the characters of `y`.
|
||||
*/
|
||||
template <class T>
|
||||
requires std::same_as<T, std::string_view>
|
||||
bool
|
||||
operator==(Value const& x, T y)
|
||||
{
|
||||
if (!x.isString())
|
||||
return false;
|
||||
|
||||
// A string `Value` can hold a null pointer, which names no characters, so it equals no view.
|
||||
char const* const s = x.asCString();
|
||||
return s != nullptr && std::string_view{s} == y;
|
||||
}
|
||||
|
||||
bool
|
||||
operator<(Value const&, Value const&);
|
||||
|
||||
|
||||
@@ -6,31 +6,21 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* API version numbers used in later API versions
|
||||
* The `api_version` numbers this server serves.
|
||||
*
|
||||
* Requests with a version number in the range
|
||||
* [apiMinimumSupportedVersion, apiMaximumSupportedVersion]
|
||||
* are supported.
|
||||
* A request naming a version in [kApiMinimumSupportedVersion,
|
||||
* kApiMaximumSupportedVersion] is served. With `[beta_rpc_api]` set to `1` in
|
||||
* the config the range extends to kApiBetaVersion.
|
||||
*
|
||||
* If [beta_rpc_api] is enabled in config, the version numbers
|
||||
* in the range [apiMinimumSupportedVersion, apiBetaVersion]
|
||||
* are supported.
|
||||
*
|
||||
* Network Requests without explicit version numbers use
|
||||
* apiVersionIfUnspecified. apiVersionIfUnspecified is 1,
|
||||
* because all the RPC requests with a version >= 2 must
|
||||
* explicitly specify the version in the requests.
|
||||
* Note that apiVersionIfUnspecified will be lower than
|
||||
* apiMinimumSupportedVersion when we stop supporting API
|
||||
* version 1.
|
||||
*
|
||||
* Command line Requests use apiCommandLineVersion.
|
||||
* A request naming no version is served at kApiVersionIfUnspecified, which is 1
|
||||
* because a request wanting any later version states it.
|
||||
*/
|
||||
|
||||
namespace rpc {
|
||||
@@ -57,6 +47,16 @@ static_assert(kApiMaximumSupportedVersion >= kApiMinimumSupportedVersion);
|
||||
static_assert(kApiBetaVersion >= kApiMaximumSupportedVersion);
|
||||
static_assert(kApiMaximumValidVersion >= kApiMaximumSupportedVersion);
|
||||
|
||||
/**
|
||||
* Values accepted in the `ripplerpc` request field, which selects the shape of
|
||||
* the JSON-RPC reply envelope. Distinct from `kJsonRpcVersion` in JsonRpc.h,
|
||||
* which names the JSON-RPC protocol itself, and from the `api_version`
|
||||
* constants above, which select the content of the response.
|
||||
*/
|
||||
inline constexpr std::string_view kRippleRpcVersion1{"1.0"};
|
||||
inline constexpr std::string_view kRippleRpcVersion2{"2.0"};
|
||||
inline constexpr std::string_view kRippleRpcVersion3{"3.0"};
|
||||
|
||||
inline void
|
||||
setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -180,11 +200,6 @@ struct ErrorInfo
|
||||
{
|
||||
}
|
||||
|
||||
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message)
|
||||
: code(code), token(token), message(message), httpStatus(200)
|
||||
{
|
||||
}
|
||||
|
||||
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message, int httpStatus)
|
||||
: code(code), token(token), message(message), httpStatus(httpStatus)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
/**
|
||||
* Constants of the JSON-RPC 2.0 protocol itself.
|
||||
*
|
||||
* Kept apart from the `api_version` and `ripplerpc` constants in ApiVersion.h,
|
||||
* which go when support for API versions 1 and 2 goes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Value of the `jsonrpc` member of a request and of its reply, fixed at "2.0"
|
||||
* by the JSON-RPC specification.
|
||||
*/
|
||||
inline constexpr std::string_view kJsonRpcVersion{"2.0"};
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -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)
|
||||
@@ -559,7 +559,7 @@ JSS(signing_key); // out: NetworkOPs
|
||||
JSS(signing_keys); // out: ValidatorList
|
||||
JSS(signing_time); // out: NetworkOPs
|
||||
JSS(signer_lists); // in/out: AccountInfo
|
||||
JSS(size); // out: get_aggregate_price
|
||||
JSS(size); // out: get_aggregate_price, ServerHandler
|
||||
JSS(snapshot); // in: Subscribe
|
||||
JSS(source_account); // in: PathRequest, RipplePathFind
|
||||
JSS(source_amount); // in: PathRequest, RipplePathFind
|
||||
|
||||
@@ -7,6 +7,18 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Writes an HTTP reply carrying @p strMsg with status @p nStatus to @p output,
|
||||
* and logs the status at trace. The body is not logged here: it may carry a
|
||||
* credential this library cannot mask, so the caller logs it masked.
|
||||
*
|
||||
* A 401 with an empty body is answered with the fixed authentication page.
|
||||
*
|
||||
* @param nStatus The HTTP status code.
|
||||
* @param strMsg The body.
|
||||
* @param output Where the reply bytes are written.
|
||||
* @param j The journal the status is logged to.
|
||||
*/
|
||||
void
|
||||
httpReply(int nStatus, std::string const& strMsg, json::Output const&, beast::Journal j);
|
||||
|
||||
|
||||
@@ -14,35 +14,112 @@ private:
|
||||
|
||||
public:
|
||||
SHAMapAddNode();
|
||||
|
||||
/**
|
||||
* Record one node that was rejected.
|
||||
*/
|
||||
void
|
||||
incInvalid();
|
||||
|
||||
/**
|
||||
* Record one node that produced a good result.
|
||||
*/
|
||||
void
|
||||
incUseful();
|
||||
|
||||
/**
|
||||
* Record one node that was not needed: the map already held it, or the map
|
||||
* or its acquisition was no longer taking nodes. isGood() counts it on the
|
||||
* accepted side.
|
||||
*/
|
||||
void
|
||||
incDuplicate();
|
||||
void
|
||||
reset();
|
||||
|
||||
/**
|
||||
* @return How many nodes produced a good result.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getGood() const;
|
||||
|
||||
/**
|
||||
* @return How many nodes this code rejected, which is not how many the
|
||||
* batch was given: a batch that stops at its first bad node counts
|
||||
* one, and a batch that carries on counts each.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getBad() const;
|
||||
|
||||
/**
|
||||
* @return How many nodes were not needed, whether already held or offered
|
||||
* to a map no longer taking nodes, tallied apart from the good and
|
||||
* bad counts.
|
||||
*/
|
||||
[[nodiscard]] int
|
||||
getDuplicate() const;
|
||||
|
||||
/**
|
||||
* Whether nodes that produced a good result or were not needed outnumber
|
||||
* the ones rejected.
|
||||
*
|
||||
* @return Whether the batch was good.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isGood() const;
|
||||
|
||||
/**
|
||||
* @return Whether at least one node in the batch was rejected.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isInvalid() const;
|
||||
|
||||
/**
|
||||
* @return Whether at least one node in the batch produced a good result.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isUseful() const;
|
||||
|
||||
/**
|
||||
* @return A verdict recording one node that was not needed.
|
||||
*/
|
||||
static SHAMapAddNode
|
||||
duplicate();
|
||||
|
||||
/**
|
||||
* @return A verdict recording one useful node.
|
||||
*/
|
||||
static SHAMapAddNode
|
||||
useful();
|
||||
|
||||
/**
|
||||
* @return A verdict recording one invalid node.
|
||||
*/
|
||||
static SHAMapAddNode
|
||||
invalid();
|
||||
|
||||
/**
|
||||
* Clear every count back to zero.
|
||||
*/
|
||||
void
|
||||
reset();
|
||||
|
||||
/**
|
||||
* Render the tally as a log line.
|
||||
*
|
||||
* @return The tally, e.g. "good:2 bad:1 dupe:1", or "no nodes processed" if
|
||||
* every count is zero.
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
get() const;
|
||||
|
||||
/**
|
||||
* Add another verdict's counts into this one.
|
||||
*
|
||||
* @param n The verdict to add.
|
||||
* @return This verdict, updated.
|
||||
*/
|
||||
SHAMapAddNode&
|
||||
operator+=(SHAMapAddNode const& n);
|
||||
|
||||
static SHAMapAddNode
|
||||
duplicate();
|
||||
static SHAMapAddNode
|
||||
useful();
|
||||
static SHAMapAddNode
|
||||
invalid();
|
||||
|
||||
private:
|
||||
SHAMapAddNode(int good, int bad, int duplicate);
|
||||
};
|
||||
@@ -74,18 +151,30 @@ SHAMapAddNode::incDuplicate()
|
||||
++duplicate_;
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMapAddNode::reset()
|
||||
{
|
||||
good_ = bad_ = duplicate_ = 0;
|
||||
}
|
||||
|
||||
inline int
|
||||
SHAMapAddNode::getGood() const
|
||||
{
|
||||
return good_;
|
||||
}
|
||||
|
||||
inline int
|
||||
SHAMapAddNode::getBad() const
|
||||
{
|
||||
return bad_;
|
||||
}
|
||||
|
||||
inline int
|
||||
SHAMapAddNode::getDuplicate() const
|
||||
{
|
||||
return duplicate_;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMapAddNode::isGood() const
|
||||
{
|
||||
return (good_ + duplicate_) > bad_;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMapAddNode::isInvalid() const
|
||||
{
|
||||
@@ -98,22 +187,6 @@ SHAMapAddNode::isUseful() const
|
||||
return good_ > 0;
|
||||
}
|
||||
|
||||
inline SHAMapAddNode&
|
||||
SHAMapAddNode::operator+=(SHAMapAddNode const& n)
|
||||
{
|
||||
good_ += n.good_;
|
||||
bad_ += n.bad_;
|
||||
duplicate_ += n.duplicate_;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline bool
|
||||
SHAMapAddNode::isGood() const
|
||||
{
|
||||
return (good_ + duplicate_) > bad_;
|
||||
}
|
||||
|
||||
inline SHAMapAddNode
|
||||
SHAMapAddNode::duplicate()
|
||||
{
|
||||
@@ -132,6 +205,12 @@ SHAMapAddNode::invalid()
|
||||
return SHAMapAddNode(0, 1, 0);
|
||||
}
|
||||
|
||||
inline void
|
||||
SHAMapAddNode::reset()
|
||||
{
|
||||
good_ = bad_ = duplicate_ = 0;
|
||||
}
|
||||
|
||||
inline std::string
|
||||
SHAMapAddNode::get() const
|
||||
{
|
||||
@@ -160,4 +239,14 @@ SHAMapAddNode::get() const
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline SHAMapAddNode&
|
||||
SHAMapAddNode::operator+=(SHAMapAddNode const& n)
|
||||
{
|
||||
good_ += n.good_;
|
||||
bad_ += n.bad_;
|
||||
duplicate_ += n.duplicate_;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace json {
|
||||
@@ -241,10 +242,14 @@ Value::Value(xrpl::Number const& value) : type_(ValueType::String), allocated_(t
|
||||
value_.stringVal = valueAllocator()->duplicateStringValue(tmp.c_str(), tmp.length());
|
||||
}
|
||||
|
||||
Value::Value(std::string const& value) : type_(ValueType::String), allocated_(true)
|
||||
Value::Value(std::string const& value) : Value(std::string_view{value})
|
||||
{
|
||||
value_.stringVal =
|
||||
valueAllocator()->duplicateStringValue(value.c_str(), (unsigned int)value.length());
|
||||
}
|
||||
|
||||
Value::Value(std::string_view value) : type_(ValueType::String), allocated_(true)
|
||||
{
|
||||
value_.stringVal = valueAllocator()->duplicateStringValue(
|
||||
value.data(), static_cast<unsigned int>(value.length()));
|
||||
}
|
||||
|
||||
Value::Value(StaticString const& value) : type_(ValueType::String)
|
||||
|
||||
@@ -20,88 +20,102 @@ namespace detail {
|
||||
// will remain in the object file. But the string literals will remain.
|
||||
//
|
||||
// There's a certain amount of tension in determining the correct HTTP
|
||||
// status to associate with a given RPC error. Initially all RPC errors
|
||||
// returned 200 (OK). And that's the default behavior if no HTTP status code
|
||||
// is specified below.
|
||||
// status to associate with a given RPC error. Every row below names its
|
||||
// HTTP status, and a row that omits it does not compile, since ErrorInfo has
|
||||
// no constructor that leaves the status out.
|
||||
//
|
||||
// The codes currently selected target the load balancer fail-over use case.
|
||||
// If a query fails on one node but is likely to have a positive outcome
|
||||
// on a different node, then the failure should return a 4xx/5xx range
|
||||
// status code.
|
||||
|
||||
// The rows below are aligned by hand into four columns, so that a wrong status or a mistyped token
|
||||
// is visible by scanning one column. That cannot be had inside the 100-column limit once the
|
||||
// longest enumerator and the longest message sit in one row. So this region is exempt from the
|
||||
// limit by design rather than by oversight.
|
||||
// clang-format off
|
||||
static constexpr ErrorInfo kUnorderedErrorInfos[]{
|
||||
{RpcActMalformed, "actMalformed", "Account malformed."},
|
||||
{RpcActNotFound, "actNotFound", "Account not found."},
|
||||
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned."},
|
||||
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed."},
|
||||
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
|
||||
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
|
||||
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
|
||||
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
|
||||
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
|
||||
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
|
||||
{RpcBadMarket, "badMarket", "No such market.", 404},
|
||||
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
|
||||
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
|
||||
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
|
||||
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
|
||||
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
|
||||
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
|
||||
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
|
||||
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
|
||||
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
|
||||
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
|
||||
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
|
||||
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
|
||||
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
|
||||
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
|
||||
{RpcForbidden, "forbidden", "Bad credentials.", 403},
|
||||
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
|
||||
{RpcInternal, "internal", "Internal error.", 500},
|
||||
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
|
||||
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
|
||||
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
|
||||
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
|
||||
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
|
||||
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
|
||||
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
|
||||
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
|
||||
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
|
||||
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
|
||||
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
|
||||
{RpcNotImpl, "notImpl", "Not implemented.", 501},
|
||||
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
|
||||
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
|
||||
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
|
||||
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
|
||||
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
|
||||
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
|
||||
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
|
||||
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
|
||||
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
|
||||
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
|
||||
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
|
||||
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
|
||||
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
|
||||
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
|
||||
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
|
||||
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
|
||||
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
|
||||
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
|
||||
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
|
||||
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
|
||||
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
|
||||
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
|
||||
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
|
||||
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
|
||||
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
|
||||
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
|
||||
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
|
||||
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
|
||||
{RpcDomainMalformed, "domainMalformed", "Domain is malformed.", 400},
|
||||
{RpcEntryNotFound, "entryNotFound", "Entry not found.", 400},
|
||||
{RpcUnexpectedLedgerType, "unexpectedLedgerType", "Unexpected ledger type.", 400},
|
||||
{RpcActMalformed, "actMalformed", "Account malformed.", 400},
|
||||
{RpcActNotFound, "actNotFound", "Account not found.", 404},
|
||||
{RpcAlreadyMultisig, "alreadyMultisig", "Already multisigned.", 400},
|
||||
{RpcAlreadySingleSig, "alreadySingleSig", "Already single-signed.", 400},
|
||||
{RpcAmendmentBlocked, "amendmentBlocked", "Amendment blocked, need upgrade.", 503},
|
||||
{RpcExpiredValidatorList, "unlBlocked", "Validator list expired.", 503},
|
||||
{RpcAtxDeprecated, "deprecated", "Use the new API or specify a ledger range.", 400},
|
||||
{RpcBadKeyType, "badKeyType", "Bad key type.", 400},
|
||||
{RpcBadFeature, "badFeature", "Feature unknown or invalid.", 500},
|
||||
{RpcBadIssuer, "badIssuer", "Issuer account malformed.", 400},
|
||||
{RpcBadMarket, "badMarket", "No such market.", 404},
|
||||
{RpcBadSecret, "badSecret", "Secret does not match account.", 403},
|
||||
{RpcBadSeed, "badSeed", "Disallowed seed.", 403},
|
||||
{RpcBadSyntax, "badSyntax", "Syntax error.", 400},
|
||||
{RpcChannelMalformed, "channelMalformed", "Payment channel is malformed.", 400},
|
||||
{RpcChannelAmtMalformed, "channelAmtMalformed", "Payment channel amount is malformed.", 400},
|
||||
{RpcCommandMissing, "commandMissing", "Missing command entry.", 400},
|
||||
{RpcDbDeserialization, "dbDeserialization", "Database deserialization error.", 502},
|
||||
{RpcDstActMalformed, "dstActMalformed", "Destination account is malformed.", 400},
|
||||
{RpcDstActMissing, "dstActMissing", "Destination account not provided.", 400},
|
||||
{RpcDstActNotFound, "dstActNotFound", "Destination account not found.", 404},
|
||||
{RpcDstAmtMalformed, "dstAmtMalformed", "Destination amount/currency/issuer is malformed.", 400},
|
||||
{RpcDstAmtMissing, "dstAmtMissing", "Destination amount/currency/issuer is missing.", 400},
|
||||
{RpcDstIsrMalformed, "dstIsrMalformed", "Destination issuer is malformed.", 400},
|
||||
{RpcExcessiveLgrRange, "excessiveLgrRange", "Ledger range exceeds 1000.", 400},
|
||||
{RpcForbidden, "forbidden", "Bad credentials.", 403},
|
||||
{RpcHighFee, "highFee", "Current transaction fee exceeds your limit.", 402},
|
||||
{RpcInternal, "internal", "Internal error.", 500},
|
||||
{RpcInvalidLgrRange, "invalidLgrRange", "Ledger range is invalid.", 400},
|
||||
{RpcInvalidParams, "invalidParams", "Invalid parameters.", 400},
|
||||
{RpcInvalidHotwallet, "invalidHotWallet", "Invalid hotwallet.", 400},
|
||||
{RpcIssueMalformed, "issueMalformed", "Issue is malformed.", 400},
|
||||
{RpcJsonRpc, "json_rpc", "JSON-RPC transport error.", 500},
|
||||
{RpcLgrIdxsInvalid, "lgrIdxsInvalid", "Ledger indexes invalid.", 400},
|
||||
{RpcLgrIdxMalformed, "lgrIdxMalformed", "Ledger index malformed.", 400},
|
||||
{RpcLgrNotFound, "lgrNotFound", "Ledger not found.", 404},
|
||||
{RpcLgrNotValidated, "lgrNotValidated", "Ledger not validated.", 202},
|
||||
{RpcMasterDisabled, "masterDisabled", "Master key is disabled.", 403},
|
||||
{RpcNotEnabled, "notEnabled", "Not enabled in configuration.", 501},
|
||||
{RpcNotImpl, "notImpl", "Not implemented.", 501},
|
||||
{RpcNotReady, "notReady", "Not ready to handle this request.", 503},
|
||||
{RpcNotSupported, "notSupported", "Operation not supported.", 501},
|
||||
{RpcNoClosed, "noClosed", "Closed ledger is unavailable.", 503},
|
||||
{RpcNoCurrent, "noCurrent", "Current ledger is unavailable.", 503},
|
||||
{RpcNotSynced, "notSynced", "Not synced to the network.", 503},
|
||||
{RpcNoEvents, "noEvents", "Current transport does not support events.", 405},
|
||||
{RpcNoNetwork, "noNetwork", "Not synced to the network.", 503},
|
||||
{RpcWrongNetwork, "wrongNetwork", "Wrong network.", 503},
|
||||
{RpcNoPermission, "noPermission", "You don't have permission for this command.", 401},
|
||||
{RpcNoPfRequest, "noPathRequest", "No pathfinding request in progress.", 404},
|
||||
{RpcObjectNotFound, "objectNotFound", "The requested object was not found.", 404},
|
||||
{RpcPublicMalformed, "publicMalformed", "Public key is malformed.", 400},
|
||||
{RpcSendmaxMalformed, "sendMaxMalformed", "SendMax amount malformed.", 400},
|
||||
{RpcSigningMalformed, "signingMalformed", "Signing of transaction is malformed.", 400},
|
||||
{RpcSlowDown, "slowDown", "You are placing too much load on the server.", 429},
|
||||
{RpcSrcActMalformed, "srcActMalformed", "Source account is malformed.", 400},
|
||||
{RpcSrcActMissing, "srcActMissing", "Source account not provided.", 400},
|
||||
{RpcSrcActNotFound, "srcActNotFound", "Source account not found.", 404},
|
||||
{RpcDelegateActNotFound, "delegateActNotFound", "Delegate account not found.", 404},
|
||||
{RpcSrcCurMalformed, "srcCurMalformed", "Source currency is malformed.", 400},
|
||||
{RpcSrcIsrMalformed, "srcIsrMalformed", "Source issuer is malformed.", 400},
|
||||
{RpcStreamMalformed, "malformedStream", "Stream malformed.", 400},
|
||||
{RpcTooBusy, "tooBusy", "The server is too busy to help you now.", 503},
|
||||
{RpcTxnNotFound, "txnNotFound", "Transaction not found.", 404},
|
||||
{RpcUnknownCommand, "unknownCmd", "Unknown method.", 405},
|
||||
{RpcOracleMalformed, "oracleMalformed", "Oracle request is malformed.", 400},
|
||||
{RpcBadCredentials, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
|
||||
{RpcTxSigned, "transactionSigned", "Transaction should not be signed.", 400},
|
||||
{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
|
||||
|
||||
|
||||
@@ -33,7 +33,10 @@ getHTTPHeaderTimestamp()
|
||||
void
|
||||
httpReply(int nStatus, std::string const& content, json::Output const& output, beast::Journal j)
|
||||
{
|
||||
JLOG(j.trace()) << "HTTP Reply " << nStatus << " " << content;
|
||||
// The status only. The body is a reply, which carries a credential when the
|
||||
// command is a keygen, and this library cannot mask one: the caller logs the
|
||||
// masked body at debug.
|
||||
JLOG(j.trace()) << "HTTP Reply " << nStatus;
|
||||
|
||||
if (content.empty() && nStatus == 401)
|
||||
{
|
||||
|
||||
@@ -95,20 +95,40 @@ EscrowFinish::preflightSigValidated(PreflightContext const& ctx)
|
||||
auto& router = ctx.registry.get().getHashRouter();
|
||||
|
||||
auto const id = ctx.tx.getTransactionID();
|
||||
auto const flags = router.getFlags(id);
|
||||
auto flags = router.getFlags(id);
|
||||
|
||||
// If we haven't checked the condition, check it
|
||||
// now. Whether it passes or not isn't important
|
||||
// in preflight.
|
||||
// now. Other than a malformed condition, whether
|
||||
// it passes or not isn't important in preflight.
|
||||
if (!any(flags & (kSfCfInvalid | kSfCfValid)))
|
||||
{
|
||||
if (checkCondition(*fb, *cb))
|
||||
{
|
||||
router.setFlags(id, kSfCfValid);
|
||||
flags = kSfCfValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
router.setFlags(id, kSfCfInvalid);
|
||||
flags = kSfCfInvalid;
|
||||
}
|
||||
}
|
||||
|
||||
// The invalid flag also covers a bad fulfillment, so
|
||||
// check the condition itself. This runs whether or not
|
||||
// the flag was cached, so the result is the same on
|
||||
// every node.
|
||||
if (any(flags & kSfCfInvalid) && ctx.rules.enabled(fixCleanup3_5_0))
|
||||
{
|
||||
using namespace xrpl::cryptoconditions;
|
||||
|
||||
std::error_code ec;
|
||||
|
||||
auto const condition = Condition::deserialize(*cb, ec);
|
||||
if (!condition)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "Malformed condition during escrow finish: " << ec.message();
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -941,41 +941,43 @@ struct Escrow_test : public beast::unit_test::Suite
|
||||
|
||||
// Now, try to fulfill using the same sequence of
|
||||
// malformed conditions.
|
||||
TER const malformedCondTer =
|
||||
features[fixCleanup3_5_0] ? TER{temMALFORMED} : TER{tecCRYPTOCONDITION_ERROR};
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(Slice{cp, cs}),
|
||||
escrow::kFulfillment(Slice{fp, fs}),
|
||||
Fee(150 * baseFee),
|
||||
Ter(tecCRYPTOCONDITION_ERROR));
|
||||
Ter(malformedCondTer));
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(Slice{cp, cs - 1}),
|
||||
escrow::kFulfillment(Slice{fp, fs}),
|
||||
Fee(150 * baseFee),
|
||||
Ter(tecCRYPTOCONDITION_ERROR));
|
||||
Ter(malformedCondTer));
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(Slice{cp, cs - 2}),
|
||||
escrow::kFulfillment(Slice{fp, fs}),
|
||||
Fee(150 * baseFee),
|
||||
Ter(tecCRYPTOCONDITION_ERROR));
|
||||
Ter(malformedCondTer));
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(Slice{cp + 1, cs - 1}),
|
||||
escrow::kFulfillment(Slice{fp, fs}),
|
||||
Fee(150 * baseFee),
|
||||
Ter(tecCRYPTOCONDITION_ERROR));
|
||||
Ter(malformedCondTer));
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(Slice{cp + 1, cs - 3}),
|
||||
escrow::kFulfillment(Slice{fp, fs}),
|
||||
Fee(150 * baseFee),
|
||||
Ter(tecCRYPTOCONDITION_ERROR));
|
||||
Ter(malformedCondTer));
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(Slice{cp + 2, cs - 2}),
|
||||
escrow::kFulfillment(Slice{fp, fs}),
|
||||
Fee(150 * baseFee),
|
||||
Ter(tecCRYPTOCONDITION_ERROR));
|
||||
Ter(malformedCondTer));
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(Slice{cp + 2, cs - 3}),
|
||||
escrow::kFulfillment(Slice{fp, fs}),
|
||||
Fee(150 * baseFee),
|
||||
Ter(tecCRYPTOCONDITION_ERROR));
|
||||
Ter(malformedCondTer));
|
||||
|
||||
// Now, using the correct kCondition, try malformed fulfillments:
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
@@ -1043,11 +1045,13 @@ struct Escrow_test : public beast::unit_test::Suite
|
||||
escrow::kCondition(escrow::kCb3),
|
||||
escrow::kCancelTime(env.now() + 1s));
|
||||
|
||||
TER const emptyCondTer =
|
||||
features[fixCleanup3_5_0] ? TER{temMALFORMED} : TER{tecCRYPTOCONDITION_ERROR};
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(Slice{}),
|
||||
escrow::kFulfillment(Slice{}),
|
||||
Fee(150 * baseFee),
|
||||
Ter(tecCRYPTOCONDITION_ERROR));
|
||||
Ter(emptyCondTer));
|
||||
env(escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(escrow::kCb3),
|
||||
escrow::kFulfillment(Slice{}),
|
||||
@@ -1057,7 +1061,7 @@ struct Escrow_test : public beast::unit_test::Suite
|
||||
escrow::kCondition(Slice{}),
|
||||
escrow::kFulfillment(escrow::kFb3),
|
||||
Fee(150 * baseFee),
|
||||
Ter(tecCRYPTOCONDITION_ERROR));
|
||||
Ter(emptyCondTer));
|
||||
|
||||
// Assemble finish that is missing the Condition or the Fulfillment
|
||||
// since either both must be present, or neither can:
|
||||
@@ -1097,6 +1101,41 @@ struct Escrow_test : public beast::unit_test::Suite
|
||||
escrow::kCancelTime(env.now() + 1s),
|
||||
Ter(temMALFORMED));
|
||||
}
|
||||
{ // Test finish with a malformed condition
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(5000), "alice", "bob", "carol");
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq("alice");
|
||||
env(escrow::create("alice", "carol", XRP(1000)),
|
||||
escrow::kCondition(escrow::kCb1),
|
||||
escrow::kCancelTime(env.now() + 100s));
|
||||
env.close();
|
||||
|
||||
// Does not deserialize as a condition.
|
||||
std::array<std::uint8_t, 3> const malformed = {{0x78, 0x78, 0x78}};
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
auto const bobBalance = env.balance("bob");
|
||||
bool const fixEnabled = features[fixCleanup3_5_0];
|
||||
auto const jt = env.jt(
|
||||
escrow::finish("bob", "alice", seq),
|
||||
escrow::kCondition(malformed),
|
||||
escrow::kFulfillment(escrow::kFb1),
|
||||
Fee(150 * baseFee));
|
||||
env(jt, Ter(fixEnabled ? TER{temMALFORMED} : TER{tecCRYPTOCONDITION_ERROR}));
|
||||
// Run preflight on the same transaction again, now with the
|
||||
// HashRouter flag cached. The result must not change.
|
||||
auto const pf =
|
||||
preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal);
|
||||
BEAST_EXPECT(pf.ter == (fixEnabled ? TER{temMALFORMED} : TER{tesSUCCESS}));
|
||||
env.close();
|
||||
|
||||
// Before the fix, the transaction is included and charges a fee.
|
||||
env.require(
|
||||
Balance("bob", fixEnabled ? bobBalance : bobBalance - drops(150 * baseFee)));
|
||||
BEAST_EXPECT(env.le(keylet::escrow(Account("alice").id(), SeqProxy::rawSequence(seq))));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1642,6 +1681,7 @@ public:
|
||||
FeatureBitset const all{testableAmendments()};
|
||||
testWithFeats(all);
|
||||
testWithFeats(all - featureTokenEscrow);
|
||||
testWithFeats(all - fixCleanup3_5_0);
|
||||
testTags(all - fixIncludeKeyletFields);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2419,8 +2419,7 @@ public:
|
||||
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jrr));
|
||||
BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -2170,8 +2170,7 @@ public:
|
||||
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jrr));
|
||||
BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
#include <xrpl/protocol/Book.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/PathAsset.h>
|
||||
#include <xrpl/protocol/Quality.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
@@ -363,6 +365,35 @@ getAccountLines(Env& env, AccountID const& acctId, IOU... ious)
|
||||
[[nodiscard]] bool
|
||||
checkArraySize(json::Value const& val, unsigned int size);
|
||||
|
||||
/**
|
||||
* Reports whether @p reply carries the version 2 envelope: `jsonrpc` equal to
|
||||
* rpc::kJsonRpcVersion and `ripplerpc` equal to rpc::kRippleRpcVersion2, both
|
||||
* present.
|
||||
*
|
||||
* @param reply The reply to inspect.
|
||||
* @return True when both members are present with those values.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
inline bool
|
||||
hasEnvelope2(json::Value const& reply)
|
||||
{
|
||||
return reply.isMember(jss::jsonrpc) && reply[jss::jsonrpc] == rpc::kJsonRpcVersion &&
|
||||
reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc] == rpc::kRippleRpcVersion2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the version 2 envelope on @p request: `jsonrpc` to rpc::kJsonRpcVersion
|
||||
* and `ripplerpc` to rpc::kRippleRpcVersion2.
|
||||
*
|
||||
* @param request The request to stamp.
|
||||
*/
|
||||
inline void
|
||||
setEnvelope2(json::Value& request)
|
||||
{
|
||||
request[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
request[jss::ripplerpc] = rpc::kRippleRpcVersion2;
|
||||
}
|
||||
|
||||
// Helper function that returns the owner count on an account.
|
||||
std::uint32_t
|
||||
ownerCount(test::jtx::Env const& env, test::jtx::Account const& account);
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
|
||||
@@ -163,8 +165,8 @@ public:
|
||||
jr[jss::method] = cmd;
|
||||
if (rpcVersion_ == 2)
|
||||
{
|
||||
jr[jss::jsonrpc] = "2.0";
|
||||
jr[jss::ripplerpc] = "2.0";
|
||||
jr[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
jr[jss::ripplerpc] = rpc::kRippleRpcVersion2;
|
||||
jr[jss::id] = 5;
|
||||
}
|
||||
if (params)
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
|
||||
@@ -207,8 +209,8 @@ public:
|
||||
if (rpcVersion_ == 2)
|
||||
{
|
||||
jp[jss::method] = cmd;
|
||||
jp[jss::jsonrpc] = "2.0";
|
||||
jp[jss::ripplerpc] = "2.0";
|
||||
jp[jss::jsonrpc] = rpc::kJsonRpcVersion;
|
||||
jp[jss::ripplerpc] = rpc::kRippleRpcVersion2;
|
||||
jp[jss::id] = 5;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/multisign.h>
|
||||
@@ -371,8 +372,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
info.isMember(jss::result) && info[jss::result].isMember(jss::account_data));
|
||||
BEAST_EXPECT(!info[jss::result][jss::account_data].isMember(jss::signer_lists));
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -385,8 +385,7 @@ public:
|
||||
auto const& signerLists = data[jss::signer_lists];
|
||||
BEAST_EXPECT(signerLists.isArray());
|
||||
BEAST_EXPECT(signerLists.size() == 0);
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
|
||||
}
|
||||
{
|
||||
@@ -396,8 +395,7 @@ public:
|
||||
info[0u].isMember(jss::result) &&
|
||||
info[0u][jss::result].isMember(jss::account_data));
|
||||
BEAST_EXPECT(!info[0u][jss::result][jss::account_data].isMember(jss::signer_lists));
|
||||
BEAST_EXPECT(info[0u].isMember(jss::jsonrpc) && info[0u][jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info[0u].isMember(jss::ripplerpc) && info[0u][jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info[0u]));
|
||||
BEAST_EXPECT(info[0u].isMember(jss::id) && info[0u][jss::id] == 5);
|
||||
|
||||
BEAST_EXPECT(
|
||||
@@ -408,8 +406,7 @@ public:
|
||||
auto const& signerLists = data[jss::signer_lists];
|
||||
BEAST_EXPECT(signerLists.isArray());
|
||||
BEAST_EXPECT(signerLists.size() == 0);
|
||||
BEAST_EXPECT(info[1u].isMember(jss::jsonrpc) && info[1u][jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info[1u].isMember(jss::ripplerpc) && info[1u][jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info[1u]));
|
||||
BEAST_EXPECT(info[1u].isMember(jss::id) && info[1u][jss::id] == 6);
|
||||
}
|
||||
|
||||
@@ -424,8 +421,7 @@ public:
|
||||
BEAST_EXPECT(
|
||||
info.isMember(jss::result) && info[jss::result].isMember(jss::account_data));
|
||||
BEAST_EXPECT(!info[jss::result][jss::account_data].isMember(jss::signer_lists));
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -445,8 +441,7 @@ public:
|
||||
BEAST_EXPECT(signerEntries.size() == 1);
|
||||
auto const& entry0 = signerEntries[0u][sfSignerEntry.jsonName];
|
||||
BEAST_EXPECT(entry0[sfSignerWeight.jsonName] == 3);
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
|
||||
}
|
||||
|
||||
@@ -495,8 +490,7 @@ public:
|
||||
BEAST_EXPECT(entry.isMember(sfAccount.jsonName));
|
||||
BEAST_EXPECT(entry[sfSignerWeight.jsonName] == 1);
|
||||
}
|
||||
BEAST_EXPECT(info.isMember(jss::jsonrpc) && info[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(info.isMember(jss::ripplerpc) && info[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(info));
|
||||
BEAST_EXPECT(info.isMember(jss::id) && info[jss::id] == 6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,25 +797,21 @@ public:
|
||||
// account_lines with mal-formed json2 (missing id field).
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
}
|
||||
{
|
||||
// account_lines with no account.
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::missingFieldError(jss::account)[jss::error_message]);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -824,16 +820,14 @@ public:
|
||||
params[jss::account] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcActMalformed)[jss::error_message]);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
Account const alice{"alice"};
|
||||
@@ -843,16 +837,14 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcActNotFound)[jss::error_message]);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
env.fund(XRP(10000), alice);
|
||||
@@ -869,16 +861,14 @@ public:
|
||||
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
|
||||
BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'.");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
};
|
||||
|
||||
@@ -895,15 +885,13 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 0);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -913,16 +901,14 @@ public:
|
||||
params[jss::ledger_index] = "nonsense";
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
"Invalid field 'ledger_index', not string or number.");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -932,14 +918,12 @@ public:
|
||||
params[jss::ledger_index] = 50000;
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::error][jss::message] == "ledgerNotFound");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
// Create trust lines to share with alice.
|
||||
@@ -992,44 +976,38 @@ public:
|
||||
BEAST_EXPECT(ledger58Info.seq == 58);
|
||||
|
||||
// A re-usable test for historic ledgers.
|
||||
auto const testAccountLinesHistory = [this, &env](
|
||||
Account const& account,
|
||||
LedgerHeader const& info,
|
||||
int count) {
|
||||
// Get account_lines by ledger index.
|
||||
json::Value paramsSeq;
|
||||
paramsSeq[jss::account] = account.human();
|
||||
paramsSeq[jss::ledger_index] = info.seq;
|
||||
json::Value requestSeq;
|
||||
requestSeq[jss::method] = "account_lines";
|
||||
requestSeq[jss::jsonrpc] = "2.0";
|
||||
requestSeq[jss::ripplerpc] = "2.0";
|
||||
requestSeq[jss::id] = 5;
|
||||
requestSeq[jss::params] = paramsSeq;
|
||||
auto const linesSeq = env.rpc("json2", to_string(requestSeq));
|
||||
BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
|
||||
BEAST_EXPECT(linesSeq.isMember(jss::jsonrpc) && linesSeq[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesSeq.isMember(jss::ripplerpc) && linesSeq[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
|
||||
auto const testAccountLinesHistory =
|
||||
[this, &env](Account const& account, LedgerHeader const& info, int count) {
|
||||
// Get account_lines by ledger index.
|
||||
json::Value paramsSeq;
|
||||
paramsSeq[jss::account] = account.human();
|
||||
paramsSeq[jss::ledger_index] = info.seq;
|
||||
json::Value requestSeq;
|
||||
requestSeq[jss::method] = "account_lines";
|
||||
setEnvelope2(requestSeq);
|
||||
requestSeq[jss::id] = 5;
|
||||
requestSeq[jss::params] = paramsSeq;
|
||||
auto const linesSeq = env.rpc("json2", to_string(requestSeq));
|
||||
BEAST_EXPECT(linesSeq[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesSeq[jss::result][jss::lines].size() == count);
|
||||
BEAST_EXPECT(hasEnvelope2(linesSeq));
|
||||
BEAST_EXPECT(linesSeq.isMember(jss::id) && linesSeq[jss::id] == 5);
|
||||
|
||||
// Get account_lines by ledger hash.
|
||||
json::Value paramsHash;
|
||||
paramsHash[jss::account] = account.human();
|
||||
paramsHash[jss::ledger_hash] = to_string(info.hash);
|
||||
json::Value requestHash;
|
||||
requestHash[jss::method] = "account_lines";
|
||||
requestHash[jss::jsonrpc] = "2.0";
|
||||
requestHash[jss::ripplerpc] = "2.0";
|
||||
requestHash[jss::id] = 5;
|
||||
requestHash[jss::params] = paramsHash;
|
||||
auto const linesHash = env.rpc("json2", to_string(requestHash));
|
||||
BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
|
||||
BEAST_EXPECT(linesHash.isMember(jss::jsonrpc) && linesHash[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesHash.isMember(jss::ripplerpc) && linesHash[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
|
||||
};
|
||||
// Get account_lines by ledger hash.
|
||||
json::Value paramsHash;
|
||||
paramsHash[jss::account] = account.human();
|
||||
paramsHash[jss::ledger_hash] = to_string(info.hash);
|
||||
json::Value requestHash;
|
||||
requestHash[jss::method] = "account_lines";
|
||||
setEnvelope2(requestHash);
|
||||
requestHash[jss::id] = 5;
|
||||
requestHash[jss::params] = paramsHash;
|
||||
auto const linesHash = env.rpc("json2", to_string(requestHash));
|
||||
BEAST_EXPECT(linesHash[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesHash[jss::result][jss::lines].size() == count);
|
||||
BEAST_EXPECT(hasEnvelope2(linesHash));
|
||||
BEAST_EXPECT(linesHash.isMember(jss::id) && linesHash[jss::id] == 5);
|
||||
};
|
||||
|
||||
// Alice should have no trust lines in ledger 3.
|
||||
testAccountLinesHistory(alice, ledger3Info, 0);
|
||||
@@ -1049,8 +1027,7 @@ public:
|
||||
params[jss::ledger_index] = ledger58Info.seq;
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
@@ -1059,8 +1036,7 @@ public:
|
||||
lines[jss::error][jss::message] ==
|
||||
"Exactly one of 'ledger_hash' or 'ledger_index' can be "
|
||||
"specified.");
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1069,15 +1045,13 @@ public:
|
||||
params[jss::account] = alice.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 52);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1087,15 +1061,13 @@ public:
|
||||
params[jss::peer] = gw1.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(lines[jss::result][jss::lines].size() == 26);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1105,16 +1077,14 @@ public:
|
||||
params[jss::peer] = "n9MJkEKHDhy5eTLuHUQeAAjo382frHNbFK4C8hcwN4nwM2SrLdBj";
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcActMalformed)[jss::error_message]);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1124,16 +1094,14 @@ public:
|
||||
params[jss::limit] = -1;
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::expectedFieldMessage(jss::limit, "unsigned integer"));
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1143,15 +1111,13 @@ public:
|
||||
paramsA[jss::limit] = 1;
|
||||
json::Value requestA;
|
||||
requestA[jss::method] = "account_lines";
|
||||
requestA[jss::jsonrpc] = "2.0";
|
||||
requestA[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestA);
|
||||
requestA[jss::id] = 5;
|
||||
requestA[jss::params] = paramsA;
|
||||
auto const linesA = env.rpc("json2", to_string(requestA));
|
||||
BEAST_EXPECT(linesA[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesA[jss::result][jss::lines].size() == 1);
|
||||
BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesA));
|
||||
BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
|
||||
|
||||
// Pick up from where the marker left off. We should get 51.
|
||||
@@ -1161,15 +1127,13 @@ public:
|
||||
paramsB[jss::marker] = marker;
|
||||
json::Value requestB;
|
||||
requestB[jss::method] = "account_lines";
|
||||
requestB[jss::jsonrpc] = "2.0";
|
||||
requestB[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestB);
|
||||
requestB[jss::id] = 5;
|
||||
requestB[jss::params] = paramsB;
|
||||
auto const linesB = env.rpc("json2", to_string(requestB));
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 51);
|
||||
BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesB));
|
||||
BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
|
||||
|
||||
// Go again from where the marker left off, but set a limit of 3.
|
||||
@@ -1179,15 +1143,13 @@ public:
|
||||
paramsC[jss::marker] = marker;
|
||||
json::Value requestC;
|
||||
requestC[jss::method] = "account_lines";
|
||||
requestC[jss::jsonrpc] = "2.0";
|
||||
requestC[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestC);
|
||||
requestC[jss::id] = 5;
|
||||
requestC[jss::params] = paramsC;
|
||||
auto const linesC = env.rpc("json2", to_string(requestC));
|
||||
BEAST_EXPECT(linesC[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesC[jss::result][jss::lines].size() == 3);
|
||||
BEAST_EXPECT(linesC.isMember(jss::jsonrpc) && linesC[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesC.isMember(jss::ripplerpc) && linesC[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesC));
|
||||
BEAST_EXPECT(linesC.isMember(jss::id) && linesC[jss::id] == 5);
|
||||
|
||||
// Mess with the marker so it becomes bad and check for the error.
|
||||
@@ -1197,16 +1159,14 @@ public:
|
||||
paramsD[jss::marker] = marker;
|
||||
json::Value requestD;
|
||||
requestD[jss::method] = "account_lines";
|
||||
requestD[jss::jsonrpc] = "2.0";
|
||||
requestD[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestD);
|
||||
requestD[jss::id] = 5;
|
||||
requestD[jss::params] = paramsD;
|
||||
auto const linesD = env.rpc("json2", to_string(requestD));
|
||||
BEAST_EXPECT(
|
||||
linesD[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
BEAST_EXPECT(linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesD.isMember(jss::ripplerpc) && linesD[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesD));
|
||||
BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1216,16 +1176,14 @@ public:
|
||||
params[jss::marker] = true;
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
BEAST_EXPECT(
|
||||
lines[jss::error][jss::message] ==
|
||||
rpc::expectedFieldMessage(jss::marker, "string"));
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1236,8 +1194,7 @@ public:
|
||||
params[jss::peer] = gw2.human();
|
||||
json::Value request;
|
||||
request[jss::method] = "account_lines";
|
||||
request[jss::jsonrpc] = "2.0";
|
||||
request[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(request);
|
||||
request[jss::id] = 5;
|
||||
request[jss::params] = params;
|
||||
auto const lines = env.rpc("json2", to_string(request));
|
||||
@@ -1246,8 +1203,7 @@ public:
|
||||
BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
|
||||
BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
|
||||
BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
|
||||
BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(lines));
|
||||
BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
|
||||
}
|
||||
{
|
||||
@@ -1258,8 +1214,7 @@ public:
|
||||
paramsA[jss::peer] = alice.human();
|
||||
json::Value requestA;
|
||||
requestA[jss::method] = "account_lines";
|
||||
requestA[jss::jsonrpc] = "2.0";
|
||||
requestA[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestA);
|
||||
requestA[jss::id] = 5;
|
||||
requestA[jss::params] = paramsA;
|
||||
auto const linesA = env.rpc("json2", to_string(requestA));
|
||||
@@ -1268,8 +1223,7 @@ public:
|
||||
BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
|
||||
BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
|
||||
BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
|
||||
BEAST_EXPECT(linesA.isMember(jss::jsonrpc) && linesA[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesA.isMember(jss::ripplerpc) && linesA[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesA));
|
||||
BEAST_EXPECT(linesA.isMember(jss::id) && linesA[jss::id] == 5);
|
||||
|
||||
// Continue from the returned marker to make sure that works.
|
||||
@@ -1282,16 +1236,14 @@ public:
|
||||
paramsB[jss::peer] = alice.human();
|
||||
json::Value requestB;
|
||||
requestB[jss::method] = "account_lines";
|
||||
requestB[jss::jsonrpc] = "2.0";
|
||||
requestB[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(requestB);
|
||||
requestB[jss::id] = 5;
|
||||
requestB[jss::params] = paramsB;
|
||||
auto const linesB = env.rpc("json2", to_string(requestB));
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].isArray());
|
||||
BEAST_EXPECT(linesB[jss::result][jss::lines].size() == 25);
|
||||
BEAST_EXPECT(!linesB[jss::result].isMember(jss::marker));
|
||||
BEAST_EXPECT(linesB.isMember(jss::jsonrpc) && linesB[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesB.isMember(jss::ripplerpc) && linesB[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesB));
|
||||
BEAST_EXPECT(linesB.isMember(jss::id) && linesB[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -1351,15 +1303,13 @@ public:
|
||||
linesBegParams[jss::limit] = 2;
|
||||
json::Value linesBegRequest;
|
||||
linesBegRequest[jss::method] = "account_lines";
|
||||
linesBegRequest[jss::jsonrpc] = "2.0";
|
||||
linesBegRequest[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(linesBegRequest);
|
||||
linesBegRequest[jss::id] = 5;
|
||||
linesBegRequest[jss::params] = linesBegParams;
|
||||
auto const linesBeg = env.rpc("json2", to_string(linesBegRequest));
|
||||
BEAST_EXPECT(linesBeg[jss::result][jss::lines][0u][jss::currency] == "USD");
|
||||
BEAST_EXPECT(linesBeg[jss::result].isMember(jss::marker));
|
||||
BEAST_EXPECT(linesBeg.isMember(jss::jsonrpc) && linesBeg[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesBeg.isMember(jss::ripplerpc) && linesBeg[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesBeg));
|
||||
BEAST_EXPECT(linesBeg.isMember(jss::id) && linesBeg[jss::id] == 5);
|
||||
|
||||
// alice pays 100 USD to cheri.
|
||||
@@ -1373,16 +1323,14 @@ public:
|
||||
linesEndParams[jss::marker] = linesBeg[jss::result][jss::marker];
|
||||
json::Value linesEndRequest;
|
||||
linesEndRequest[jss::method] = "account_lines";
|
||||
linesEndRequest[jss::jsonrpc] = "2.0";
|
||||
linesEndRequest[jss::ripplerpc] = "2.0";
|
||||
setEnvelope2(linesEndRequest);
|
||||
linesEndRequest[jss::id] = 5;
|
||||
linesEndRequest[jss::params] = linesEndParams;
|
||||
auto const linesEnd = env.rpc("json2", to_string(linesEndRequest));
|
||||
BEAST_EXPECT(
|
||||
linesEnd[jss::error][jss::message] ==
|
||||
rpc::makeError(RpcInvalidParams)[jss::error_message]);
|
||||
BEAST_EXPECT(linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(linesEnd.isMember(jss::ripplerpc) && linesEnd[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(linesEnd));
|
||||
BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5);
|
||||
}
|
||||
|
||||
|
||||
+21
-40
@@ -1,4 +1,5 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/domain.h>
|
||||
@@ -88,8 +89,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -126,8 +126,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -165,8 +164,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -209,8 +207,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -242,8 +239,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -288,8 +284,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -328,8 +323,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -386,8 +380,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -428,8 +421,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -487,8 +479,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -542,8 +533,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -613,8 +603,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -657,8 +646,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -731,8 +719,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -789,8 +776,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -887,8 +873,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -921,8 +906,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
@@ -952,8 +936,7 @@ public:
|
||||
auto jv = wsc->invoke("book_offers", jvParams);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
auto jrr = jv[jss::result];
|
||||
@@ -997,8 +980,7 @@ public:
|
||||
jv = wsc->invoke("book_offers", jvParams);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
jrr = jv[jss::result];
|
||||
@@ -1023,8 +1005,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", books);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h>
|
||||
@@ -22,6 +23,7 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace xrpl::test {
|
||||
@@ -254,8 +256,7 @@ class DeliveredAmount_test : public beast::unit_test::Suite
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 3);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/escrow.h>
|
||||
@@ -85,8 +86,7 @@ public:
|
||||
expect(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
expect(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
expect(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
expect(hasEnvelope2(jv));
|
||||
expect(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class MaskSecrets_test : public beast::unit_test::Suite
|
||||
{
|
||||
static constexpr char const* kMasked = "<masked>";
|
||||
static constexpr char const* kSensitive = "sensitive";
|
||||
|
||||
public:
|
||||
/**
|
||||
* Every field in `kCredentialFields` is masked, at the top level.
|
||||
*/
|
||||
void
|
||||
testEveryCredentialField()
|
||||
{
|
||||
testcase("Every credential field is masked");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
request[std::string{field}] = kSensitive;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
BEAST_EXPECTS(masked[std::string{field}] == kMasked, std::string{field});
|
||||
|
||||
// An empty list would let every assertion above pass while masking nothing.
|
||||
BEAST_EXPECT(!rpc::kCredentialFields.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* A field the list does not name is left as it was.
|
||||
*/
|
||||
void
|
||||
testNonCredentialsSurvive()
|
||||
{
|
||||
testcase("Fields that are not credentials are left alone");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::method] = "sign";
|
||||
request[jss::account] = "rSomeAccount";
|
||||
request[jss::secret] = kSensitive;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
BEAST_EXPECT(masked[jss::secret] == kMasked);
|
||||
BEAST_EXPECT(masked[jss::method] == "sign");
|
||||
BEAST_EXPECT(masked[jss::account] == "rSomeAccount");
|
||||
|
||||
// Masking replaces members rather than adding or removing them.
|
||||
BEAST_EXPECT(masked.size() == request.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* A member name is matched whole, not as a substring or case-insensitively.
|
||||
*/
|
||||
void
|
||||
testNamesAreMatchedExactly()
|
||||
{
|
||||
testcase("Only an exact field name is masked");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
for (auto const* name : {"secrets", "Secret", "my_secret", "seedling", "sec", ""})
|
||||
request[name] = kSensitive;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
for (auto const* name : {"secrets", "Secret", "my_secret", "seedling", "sec", ""})
|
||||
BEAST_EXPECTS(masked[name] == kSensitive, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* A credential is masked wherever it sits, not only at the top level.
|
||||
*
|
||||
* The JSON-RPC transport nests a request's fields inside `params`, which is
|
||||
* where a `secret` arrives.
|
||||
*/
|
||||
void
|
||||
testNesting()
|
||||
{
|
||||
testcase("A nested credential is masked at any depth");
|
||||
|
||||
// Where the JSON-RPC transport carries a credential: inside `params`.
|
||||
{
|
||||
json::Value params(json::ValueType::Object);
|
||||
params[jss::secret] = kSensitive;
|
||||
params[jss::account] = "rSomeAccount";
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::method] = "sign";
|
||||
request[jss::params] = json::ValueType::Array;
|
||||
request[jss::params][0u] = params;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
BEAST_EXPECT(masked[jss::params][0u][jss::secret] == kMasked);
|
||||
BEAST_EXPECT(masked[jss::params][0u][jss::account] == "rSomeAccount");
|
||||
BEAST_EXPECT(masked[jss::method] == "sign");
|
||||
}
|
||||
|
||||
// Where a batch carries one: inside an entry, itself inside `params`.
|
||||
{
|
||||
json::Value entry(json::ValueType::Object);
|
||||
entry[jss::id] = 2;
|
||||
entry[jss::seed] = kSensitive;
|
||||
|
||||
json::Value batch(json::ValueType::Object);
|
||||
batch[jss::method] = "batch";
|
||||
batch[jss::params] = json::ValueType::Array;
|
||||
batch[jss::params][0u] = entry;
|
||||
|
||||
auto const masked = rpc::maskSecrets(batch);
|
||||
BEAST_EXPECT(masked[jss::params][0u][jss::seed] == kMasked);
|
||||
BEAST_EXPECT(masked[jss::params][0u][jss::id] == 2);
|
||||
}
|
||||
|
||||
// A credential in an array element, which names no member of its own.
|
||||
{
|
||||
json::Value inner(json::ValueType::Object);
|
||||
inner[jss::passphrase] = kSensitive;
|
||||
|
||||
json::Value request(json::ValueType::Array);
|
||||
request[0u] = inner;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
BEAST_EXPECT(masked[0u][jss::passphrase] == kMasked);
|
||||
}
|
||||
|
||||
// Up to the parser's nesting limit, which is what bounds the recursion.
|
||||
{
|
||||
json::Value deep(json::ValueType::Object);
|
||||
deep[jss::secret] = kSensitive;
|
||||
for (unsigned i{0}; i < json::Reader::kNestLimit; ++i)
|
||||
{
|
||||
json::Value outer(json::ValueType::Object);
|
||||
outer[jss::params] = deep;
|
||||
deep = outer;
|
||||
}
|
||||
|
||||
auto const masked = rpc::maskSecrets(deep);
|
||||
auto const* level = &masked;
|
||||
for (unsigned i{0}; i < json::Reader::kNestLimit; ++i)
|
||||
level = &(*level)[jss::params];
|
||||
BEAST_EXPECT((*level)[jss::secret] == kMasked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A credential's own value is replaced whole, whatever shape it has.
|
||||
*
|
||||
* An object or array under `secret` is not walked into: the whole value is
|
||||
* the credential.
|
||||
*/
|
||||
void
|
||||
testCredentialIsNotDescendedInto()
|
||||
{
|
||||
testcase("A credential is replaced whatever it holds");
|
||||
|
||||
json::Value nested(json::ValueType::Object);
|
||||
nested["inner"] = kSensitive;
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::secret] = nested;
|
||||
request[jss::seed] = json::ValueType::Array;
|
||||
request[jss::seed][0u] = kSensitive;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
BEAST_EXPECT(masked[jss::secret] == kMasked);
|
||||
BEAST_EXPECT(masked[jss::seed] == kMasked);
|
||||
|
||||
BEAST_EXPECT(!to_string(masked).contains(kSensitive));
|
||||
}
|
||||
|
||||
/**
|
||||
* A credential that is not a string is masked too, the value being replaced
|
||||
* whole.
|
||||
*/
|
||||
void
|
||||
testNonStringCredentials()
|
||||
{
|
||||
testcase("A credential of any type is masked");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request["secret"] = 12345;
|
||||
request["seed"] = true;
|
||||
request["passphrase"] = 1.5;
|
||||
request["password"] = json::ValueType::Null;
|
||||
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
for (auto const* field : {"secret", "seed", "passphrase", "password"})
|
||||
BEAST_EXPECTS(masked[field] == kMasked, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* A value with no members is returned unchanged, masking keying on member
|
||||
* names.
|
||||
*/
|
||||
void
|
||||
testValuesWithoutMembers()
|
||||
{
|
||||
testcase("A value that has no members is returned unchanged");
|
||||
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{}).isNull());
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{kSensitive}) == kSensitive);
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{42}) == 42);
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{json::ValueType::Object}).size() == 0);
|
||||
BEAST_EXPECT(rpc::maskSecrets(json::Value{json::ValueType::Array}).size() == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The request the caller passed in is not modified.
|
||||
*
|
||||
* Every call site goes on to use the request it holds, so the mask copies.
|
||||
*/
|
||||
void
|
||||
testRequestIsNotModified()
|
||||
{
|
||||
testcase("The request passed in is left unchanged");
|
||||
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::secret] = kSensitive;
|
||||
request[jss::params] = json::ValueType::Array;
|
||||
request[jss::params][0u] = json::ValueType::Object;
|
||||
request[jss::params][0u][jss::seed] = kSensitive;
|
||||
|
||||
auto const before = to_string(request);
|
||||
auto const masked = rpc::maskSecrets(request);
|
||||
|
||||
BEAST_EXPECT(to_string(request) == before);
|
||||
BEAST_EXPECT(request[jss::secret] == kSensitive);
|
||||
BEAST_EXPECT(request[jss::params][0u][jss::seed] == kSensitive);
|
||||
BEAST_EXPECT(masked[jss::secret] == kMasked);
|
||||
}
|
||||
|
||||
/**
|
||||
* `hasSecret` reports a credential at any depth and nothing else.
|
||||
*
|
||||
* Every name on the list is reported at the top level, one nested where the
|
||||
* JSON-RPC transport puts it and one inside an array element are reported
|
||||
* too, and a value naming none, or having no members at all, is not.
|
||||
*/
|
||||
void
|
||||
testHasSecret()
|
||||
{
|
||||
testcase("A credential is detected at any depth");
|
||||
|
||||
for (auto const field : rpc::kCredentialFields)
|
||||
{
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[std::string{field}] = kSensitive;
|
||||
BEAST_EXPECTS(rpc::hasSecret(request), std::string{field});
|
||||
}
|
||||
|
||||
// Inside `params`, where the JSON-RPC transport carries a credential.
|
||||
{
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::method] = "sign";
|
||||
request[jss::params] = json::ValueType::Array;
|
||||
request[jss::params][0u] = json::ValueType::Object;
|
||||
request[jss::params][0u][jss::account] = "rSomeAccount";
|
||||
request[jss::params][0u][jss::secret] = kSensitive;
|
||||
BEAST_EXPECT(rpc::hasSecret(request));
|
||||
}
|
||||
|
||||
// Inside an array element, which names no member of its own.
|
||||
{
|
||||
json::Value request(json::ValueType::Array);
|
||||
request[0u] = json::ValueType::Object;
|
||||
request[0u][jss::passphrase] = kSensitive;
|
||||
BEAST_EXPECT(rpc::hasSecret(request));
|
||||
}
|
||||
|
||||
// No credential at any depth, and a near miss is not one.
|
||||
{
|
||||
json::Value request(json::ValueType::Object);
|
||||
request[jss::method] = "account_info";
|
||||
request[jss::params] = json::ValueType::Array;
|
||||
request[jss::params][0u] = json::ValueType::Object;
|
||||
request[jss::params][0u][jss::account] = "rSomeAccount";
|
||||
request[jss::params][0u]["secrets"] = kSensitive;
|
||||
BEAST_EXPECT(!rpc::hasSecret(request));
|
||||
}
|
||||
|
||||
// A value with no members carries none.
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{}));
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{kSensitive}));
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{42}));
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{json::ValueType::Object}));
|
||||
BEAST_EXPECT(!rpc::hasSecret(json::Value{json::ValueType::Array}));
|
||||
}
|
||||
|
||||
/**
|
||||
* `loggable` masks a value that carries a credential, then truncates at
|
||||
* `kMaxLoggedChars`; one that carries none is rendered as it is.
|
||||
*
|
||||
* The mask has to survive the cap, so the credential is placed where a
|
||||
* sorted member order puts it first.
|
||||
*/
|
||||
void
|
||||
testLoggable()
|
||||
{
|
||||
testcase("A rendering for a log line is masked and capped");
|
||||
|
||||
// Sorts before the filler, so the mask falls inside the cap.
|
||||
json::Value request;
|
||||
request[jss::secret] = kSensitive;
|
||||
request["zfiller"] = std::string(2 * rpc::kMaxLoggedChars, 'z');
|
||||
|
||||
auto const rendered = rpc::loggable(request);
|
||||
|
||||
BEAST_EXPECT(rendered.size() == rpc::kMaxLoggedChars);
|
||||
BEAST_EXPECT(rendered.contains(kMasked));
|
||||
BEAST_EXPECT(!rendered.contains(kSensitive));
|
||||
|
||||
// A short request is not padded.
|
||||
json::Value small;
|
||||
small[jss::seed] = kSensitive;
|
||||
BEAST_EXPECT(rpc::loggable(small) == to_string(rpc::maskSecrets(small)));
|
||||
|
||||
// A value with no credential is the serialized value itself, cut at the cap.
|
||||
json::Value plain;
|
||||
plain[jss::account] = "rSomeAccount";
|
||||
plain["zfiller"] = std::string(2 * rpc::kMaxLoggedChars, 'z');
|
||||
BEAST_EXPECT(rpc::loggable(plain) == to_string(plain).substr(0, rpc::kMaxLoggedChars));
|
||||
BEAST_EXPECT(rpc::loggable(small[jss::seed]) == to_string(small[jss::seed]));
|
||||
}
|
||||
|
||||
/**
|
||||
* A keygen reply is rendered for the log with no key in it.
|
||||
*
|
||||
* Every field of each reply is checked, not only the names on the list, so
|
||||
* a field the list is missing fails here. Only the fields a client may read
|
||||
* in public are allowed through.
|
||||
*/
|
||||
void
|
||||
testKeygenRepliesAreMasked()
|
||||
{
|
||||
testcase("A keygen reply is logged with every key masked");
|
||||
|
||||
static constexpr std::array<std::string_view, 6> kPublicFields{
|
||||
"account_id",
|
||||
"key_type",
|
||||
"public_key",
|
||||
"public_key_hex",
|
||||
"status",
|
||||
"validation_public_key",
|
||||
};
|
||||
|
||||
jtx::Env env{*this};
|
||||
for (auto const command : {"wallet_propose", "validation_create"})
|
||||
{
|
||||
auto const reply = env.rpc(command);
|
||||
auto const& result = reply[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == "success");
|
||||
|
||||
auto const rendered = rpc::loggable(reply);
|
||||
for (auto const& name : result.getMemberNames())
|
||||
{
|
||||
if (std::ranges::find(kPublicFields, name) != kPublicFields.end())
|
||||
continue;
|
||||
auto const& value = result[name];
|
||||
BEAST_EXPECT(value.isString() && !value.asString().empty());
|
||||
BEAST_EXPECTS(!rendered.contains(value.asString()), command + (": " + name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testEveryCredentialField();
|
||||
testNonCredentialsSurvive();
|
||||
testNamesAreMatchedExactly();
|
||||
testNesting();
|
||||
testCredentialIsNotDescendedInto();
|
||||
testNonStringCredentials();
|
||||
testValuesWithoutMembers();
|
||||
testRequestIsNotModified();
|
||||
testHasSecret();
|
||||
testLoggable();
|
||||
testKeygenRepliesAreMasked();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(MaskSecrets, rpc, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/balance.h> // IWYU pragma: keep
|
||||
@@ -38,8 +39,7 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,7 @@ public:
|
||||
auto jv = wsc->invoke("submit", payment);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefMAX_LEDGER");
|
||||
@@ -65,8 +64,7 @@ public:
|
||||
jv = wsc->invoke("submit", payment);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefPAST_SEQ");
|
||||
@@ -76,8 +74,7 @@ public:
|
||||
jv = wsc->invoke("submit", payment);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "terPRE_SEQ");
|
||||
@@ -87,8 +84,7 @@ public:
|
||||
jv = wsc->invoke("submit", payment);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
@@ -100,8 +96,7 @@ public:
|
||||
jv = wsc->invoke("ledger_accept");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
|
||||
@@ -128,8 +123,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -164,8 +158,7 @@ public:
|
||||
jv = wsc->invoke("submit", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
@@ -187,8 +180,7 @@ public:
|
||||
jv = wsc->invoke("account_tx", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
|
||||
@@ -218,8 +210,7 @@ public:
|
||||
jv = wsc->invoke("submit", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
@@ -228,8 +219,7 @@ public:
|
||||
jv = wsc->invoke("ledger_accept");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
|
||||
@@ -247,8 +237,7 @@ public:
|
||||
jv = wsc->invoke("subscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -260,8 +249,7 @@ public:
|
||||
auto jv = wsc->invoke("ledger_accept");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
|
||||
@@ -281,8 +269,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -300,8 +287,7 @@ public:
|
||||
jv = wsc->invoke("subscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -313,8 +299,7 @@ public:
|
||||
auto jv = wsc->invoke("ledger_accept");
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
|
||||
@@ -334,8 +319,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -352,8 +336,7 @@ public:
|
||||
jv = wsc->invoke("account_tx", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
|
||||
@@ -383,8 +366,7 @@ public:
|
||||
jv = wsc->invoke("subscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -399,8 +381,7 @@ public:
|
||||
jv = wsc->invoke("submit", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
|
||||
@@ -421,8 +402,7 @@ public:
|
||||
jv = wsc->invoke("unsubscribe", jv);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/WSClient.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/batch.h>
|
||||
@@ -83,8 +84,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -113,8 +113,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -149,8 +148,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 2);
|
||||
@@ -184,8 +182,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -208,8 +205,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -266,8 +262,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -281,8 +276,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -314,8 +308,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -344,8 +337,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -391,8 +383,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -414,8 +405,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -425,8 +415,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -461,8 +450,7 @@ public:
|
||||
auto jv = wsc->invoke("subscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
@@ -537,8 +525,7 @@ public:
|
||||
auto jv = wsc->invoke("unsubscribe", stream);
|
||||
if (wsc->version() == 2)
|
||||
{
|
||||
BEAST_EXPECT(jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
|
||||
BEAST_EXPECT(jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
|
||||
BEAST_EXPECT(hasEnvelope2(jv));
|
||||
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
|
||||
}
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
|
||||
@@ -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); });
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -598,6 +598,91 @@ TEST(JsonValue, compare_strings)
|
||||
#pragma pop_macro("DO_COMPARE")
|
||||
}
|
||||
|
||||
TEST(JsonValue, construct_from_string_view)
|
||||
{
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
// Characters are copied with an explicit length, so a view need not be NUL-terminated.
|
||||
EXPECT_EQ(json::Value{"2.0"sv}.asString(), "2.0");
|
||||
EXPECT_EQ(json::Value{"2.0 and more"sv.substr(0, 3)}.asString(), "2.0");
|
||||
EXPECT_EQ(json::Value{""sv}.asString(), "");
|
||||
|
||||
// A default-constructed view has a null `data()` and yields an empty string value.
|
||||
std::string_view const empty;
|
||||
json::Value const fromEmpty{empty};
|
||||
EXPECT_TRUE(fromEmpty.isString());
|
||||
EXPECT_EQ(fromEmpty.asString(), "");
|
||||
EXPECT_TRUE(fromEmpty == ""sv);
|
||||
|
||||
// The view need not outlive the call.
|
||||
json::Value copied;
|
||||
{
|
||||
std::string const owner{"borrowed"};
|
||||
copied = json::Value{std::string_view{owner}};
|
||||
}
|
||||
EXPECT_EQ(copied.asString(), "borrowed");
|
||||
|
||||
// A reader of a string value stops at the first NUL, so a view carrying one reads back short.
|
||||
json::Value const embedded{"a\0b"sv};
|
||||
EXPECT_EQ(embedded.asString(), "a");
|
||||
EXPECT_TRUE(embedded != "a\0b"sv);
|
||||
EXPECT_TRUE(embedded == "a"sv);
|
||||
EXPECT_TRUE(embedded == json::Value{"a\0b"sv});
|
||||
}
|
||||
|
||||
TEST(JsonValue, compare_string_view)
|
||||
{
|
||||
// Comparing against a view builds no temporary Value.
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
EXPECT_TRUE(json::Value{"2.0"} == "2.0"sv);
|
||||
EXPECT_FALSE(json::Value{"2.0"} != "2.0"sv);
|
||||
EXPECT_TRUE(json::Value{"2.0"} != "2.00"sv);
|
||||
EXPECT_TRUE(json::Value{"2.0"} != ""sv);
|
||||
|
||||
// A view need not be NUL-terminated, and the comparison must not read past its end.
|
||||
EXPECT_TRUE(json::Value{"2.0"} == "2.0 and more"sv.substr(0, 3));
|
||||
|
||||
EXPECT_TRUE(json::Value{""} == ""sv);
|
||||
|
||||
// A string value holding a null pointer equals nothing, not even the empty view.
|
||||
EXPECT_TRUE(json::Value{json::ValueType::String} != ""sv);
|
||||
EXPECT_TRUE(json::Value{json::ValueType::String} != "2.0"sv);
|
||||
EXPECT_TRUE(json::Value{json::ValueType::String} != json::Value{""});
|
||||
|
||||
// Nothing that is not a string equals one. Null is what an absent member reads as.
|
||||
EXPECT_TRUE(json::Value{json::ValueType::Null} != ""sv);
|
||||
EXPECT_TRUE(json::Value{json::ValueType::Null} != "2.0"sv);
|
||||
EXPECT_TRUE(json::Value{2} != "2"sv);
|
||||
EXPECT_TRUE(json::Value{true} != "true"sv);
|
||||
EXPECT_TRUE(json::Value{json::ValueType::Object} != ""sv);
|
||||
|
||||
// Only a view reaches this overload; every other spelling compares through Value.
|
||||
static constexpr json::StaticString kVersion{"2.0"};
|
||||
EXPECT_TRUE(json::Value{"2.0"} == kVersion);
|
||||
EXPECT_TRUE(json::Value{"2.1"} != kVersion);
|
||||
EXPECT_TRUE(json::Value{} != kVersion);
|
||||
|
||||
EXPECT_TRUE(json::Value{"2.0"} == "2.0");
|
||||
EXPECT_TRUE(json::Value{"2.0"} != "2.1");
|
||||
EXPECT_TRUE(json::Value{} != "2.0");
|
||||
|
||||
EXPECT_TRUE(json::Value{"2.0"} == std::string{"2.0"});
|
||||
EXPECT_TRUE(json::Value{"2.0"} != std::string{"2.1"});
|
||||
EXPECT_TRUE(json::Value{json::ValueType::Null} != std::string{"2.0"});
|
||||
|
||||
// A literal `0` also converts to a view through `char const*`, so a plain overload would make
|
||||
// this comparison ambiguous. It must compare as a number: RPCCall.cpp rejects a
|
||||
// `transaction_entry` ledger index this way.
|
||||
EXPECT_TRUE(json::Value{0} == 0);
|
||||
EXPECT_TRUE(json::Value{4294967295u} != 0);
|
||||
EXPECT_TRUE(json::Value{""} != 0);
|
||||
EXPECT_TRUE(json::Value{2} != 0);
|
||||
|
||||
// A null and an Int are different types, so the comparison is false.
|
||||
EXPECT_TRUE(json::Value{} != 0);
|
||||
}
|
||||
|
||||
TEST(JsonValue, bool)
|
||||
{
|
||||
EXPECT_FALSE(json::Value());
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <set>
|
||||
#include <type_traits>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
// A row cannot be written without a status, so no row can default to 200 again.
|
||||
static_assert(!std::is_constructible_v<rpc::ErrorInfo, ErrorCodeI, char const*, char const*>);
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* The numbers between `RpcBadSyntax` and `RpcLast` that have no row in the
|
||||
* error table: those no enumerator uses, and `RpcReportingUnsupported` (91),
|
||||
* the one enumerator the table does not list.
|
||||
*/
|
||||
std::set<int> const kGaps{
|
||||
5, 8, 20, 24, 25, 26, 27, 28, 34, 38, 39, 54, 55, 56,
|
||||
59, 60, 61, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ErrorCodes, table_gaps_report_the_unknown_placeholder)
|
||||
{
|
||||
// Codes absent from the table report the placeholder, so a scan by code must skip them.
|
||||
auto const& gap = rpc::getErrorInfo(static_cast<ErrorCodeI>(5));
|
||||
EXPECT_EQ(gap.code, RpcUnknown);
|
||||
EXPECT_STREQ(gap.token.cStr(), "unknown");
|
||||
|
||||
auto const& real = rpc::getErrorInfo(RpcInvalidParams);
|
||||
EXPECT_EQ(real.code, RpcInvalidParams);
|
||||
EXPECT_STREQ(real.token.cStr(), "invalidParams");
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, exactly_the_listed_codes_have_no_row)
|
||||
{
|
||||
// The gap set is written out by hand, so an enumerator added without a row fails here, where a
|
||||
// test asking the table which codes it knows would take the missing row for a gap.
|
||||
for (int code = RpcBadSyntax; code <= RpcLast; ++code)
|
||||
{
|
||||
bool const isGap = rpc::getErrorInfo(static_cast<ErrorCodeI>(code)).code == RpcUnknown;
|
||||
EXPECT_EQ(isGap, kGaps.contains(code)) << "code " << code;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, every_code_names_an_http_status)
|
||||
{
|
||||
// Written out here rather than read from the row: a test reading the row production reads
|
||||
// cannot tell whether the row is right.
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcActMalformed), 400);
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcActNotFound), 404);
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcAlreadyMultisig), 400);
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcAlreadySingleSig), 400);
|
||||
|
||||
// Every row names a status of its own. A code with no row reports the placeholder's, which the
|
||||
// default constructor fixes at 200.
|
||||
for (int code = RpcBadSyntax; code <= RpcLast; ++code)
|
||||
{
|
||||
auto const status = rpc::errorCodeHttpStatus(static_cast<ErrorCodeI>(code));
|
||||
if (kGaps.contains(code))
|
||||
{
|
||||
EXPECT_EQ(status, 200) << "code " << code;
|
||||
}
|
||||
else
|
||||
{
|
||||
EXPECT_NE(status, 200) << "code " << code;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#include <xrpl/shamap/SHAMapAddNode.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace xrpl::tests {
|
||||
|
||||
// get() is a log format, so its wording is pinned here, once. Every other site reads the same tally
|
||||
// by value, through the count and verdict accessors.
|
||||
TEST(SHAMapAddNode, get_names_every_non_empty_count)
|
||||
{
|
||||
EXPECT_EQ(SHAMapAddNode{}.get(), "no nodes processed");
|
||||
EXPECT_EQ(SHAMapAddNode::useful().get(), "good:1");
|
||||
EXPECT_EQ(SHAMapAddNode::invalid().get(), "bad:1");
|
||||
EXPECT_EQ(SHAMapAddNode::duplicate().get(), "dupe:1");
|
||||
|
||||
// Several of a kind are counted, and the counts are joined in a fixed order with a single
|
||||
// space, whichever order they were recorded in.
|
||||
SHAMapAddNode san;
|
||||
san.incInvalid();
|
||||
san.incUseful();
|
||||
san.incUseful();
|
||||
san.incDuplicate();
|
||||
EXPECT_EQ(san.get(), "good:2 bad:1 dupe:1");
|
||||
|
||||
san.reset();
|
||||
EXPECT_EQ(san.get(), "no nodes processed");
|
||||
}
|
||||
|
||||
// The three counts and the verdicts derived from them.
|
||||
TEST(SHAMapAddNode, counts_and_verdicts_agree)
|
||||
{
|
||||
SHAMapAddNode san;
|
||||
EXPECT_EQ(san.getGood(), 0);
|
||||
EXPECT_EQ(san.getBad(), 0);
|
||||
EXPECT_EQ(san.getDuplicate(), 0);
|
||||
EXPECT_FALSE(san.isInvalid());
|
||||
EXPECT_FALSE(san.isUseful());
|
||||
|
||||
// Good counts what produced a good result, and useful is that count being non-zero.
|
||||
san.incUseful();
|
||||
EXPECT_EQ(san.getGood(), 1);
|
||||
EXPECT_TRUE(san.isUseful());
|
||||
EXPECT_TRUE(san.isGood());
|
||||
|
||||
// A duplicate counts toward good. isUseful() here reflects the incUseful() above.
|
||||
san.incDuplicate();
|
||||
EXPECT_EQ(san.getDuplicate(), 1);
|
||||
EXPECT_FALSE(san.isInvalid());
|
||||
EXPECT_TRUE(san.isGood());
|
||||
|
||||
// Bad is a count, so a batch that carries on past a rejected node reports one per node, which
|
||||
// distinguishes "stopped on the first" from "rejected several".
|
||||
san.incInvalid();
|
||||
EXPECT_EQ(san.getBad(), 1);
|
||||
EXPECT_TRUE(san.isInvalid());
|
||||
EXPECT_TRUE(san.isGood()) << "one bad node among two accepted ones is still a good batch";
|
||||
|
||||
san.incInvalid();
|
||||
san.incInvalid();
|
||||
EXPECT_EQ(san.getGood(), 1);
|
||||
EXPECT_EQ(san.getBad(), 3);
|
||||
EXPECT_EQ(san.getDuplicate(), 1);
|
||||
EXPECT_FALSE(san.isGood()) << "more bad nodes than accepted ones is not a good batch";
|
||||
|
||||
// Adding one verdict to another sums every count.
|
||||
SHAMapAddNode total;
|
||||
total += SHAMapAddNode::useful();
|
||||
total += SHAMapAddNode::invalid();
|
||||
total += SHAMapAddNode::invalid();
|
||||
total += SHAMapAddNode::duplicate();
|
||||
EXPECT_EQ(total.getGood(), 1);
|
||||
EXPECT_EQ(total.getBad(), 2);
|
||||
EXPECT_EQ(total.getDuplicate(), 1);
|
||||
}
|
||||
|
||||
} // namespace xrpl::tests
|
||||
@@ -180,4 +180,50 @@ TEST_F(SHAMapSyncTest, sync)
|
||||
destination.invariants();
|
||||
}
|
||||
|
||||
// The duplicate verdict also answers for a node that did not need to be added, which is the half
|
||||
// of incDuplicate()'s contract that "already held" does not cover. addKnownNode() reaches it when
|
||||
// the map has stopped taking nodes, and returns there before looking at the offer at all.
|
||||
//
|
||||
// An A/B on one offer and two maps of the same shape, so the synching state is the only difference
|
||||
// between the two verdicts. Neither map holds the offered node, so a count that meant "already
|
||||
// held" would be wrong for it.
|
||||
TEST_F(SHAMapSyncTest, add_known_node_reports_duplicate_once_a_map_stops_synching)
|
||||
{
|
||||
TestNodeFamily f{j_};
|
||||
|
||||
// A well-formed inner node with one child. Its contents do not matter: both verdicts below
|
||||
// are reached without the map reading them.
|
||||
auto const makeOffer = [] {
|
||||
Serializer s;
|
||||
s.addBitString(UInt256{1});
|
||||
s.add8(0);
|
||||
s.add8(kWireTypeCompressedInner);
|
||||
return SHAMapTreeNode::makeFromWire(makeSlice(s.peekData()));
|
||||
};
|
||||
ASSERT_TRUE(makeOffer());
|
||||
|
||||
SHAMapNodeID const target{1, UInt256{}};
|
||||
|
||||
// Synching, so the offer is examined, and refused because the empty root has no branch to
|
||||
// hook it onto. The point is that the map looked.
|
||||
SHAMap synching{SHAMapType::FREE, f};
|
||||
synching.setSynching();
|
||||
|
||||
auto const examined = synching.addKnownNode(target, makeOffer(), nullptr);
|
||||
EXPECT_TRUE(examined.isInvalid());
|
||||
EXPECT_EQ(examined.getDuplicate(), 0);
|
||||
|
||||
// The same offer to a map that has stopped taking nodes.
|
||||
SHAMap stopped{SHAMapType::FREE, f};
|
||||
stopped.setSynching();
|
||||
stopped.clearSynching();
|
||||
ASSERT_FALSE(stopped.isSynching());
|
||||
|
||||
auto const notNeeded = stopped.addKnownNode(target, makeOffer(), nullptr);
|
||||
EXPECT_EQ(notNeeded.getGood(), 0);
|
||||
EXPECT_EQ(notNeeded.getBad(), 0);
|
||||
EXPECT_EQ(notNeeded.getDuplicate(), 1);
|
||||
EXPECT_TRUE(notNeeded.isGood()) << "a node that was not needed counts on the accepted side";
|
||||
}
|
||||
|
||||
} // namespace xrpl::tests
|
||||
@@ -42,7 +42,7 @@ ConsensusTransSetSF::gotNode(
|
||||
|
||||
nodeCache_.insert(nodeHash, nodeData);
|
||||
|
||||
if ((type == SHAMapNodeType::TnTransactionNm) && (nodeData.size() > 16))
|
||||
if ((type == SHAMapNodeType::TnTransactionNm) && (nodeData.size() >= kMinTxNodeBytesToParse))
|
||||
{
|
||||
// this is a transaction, and we didn't have it
|
||||
JLOG(j_.debug()) << "Node on our acquiring TX set is TXN we may not have";
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/shamap/SHAMapSyncFilter.h>
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
@@ -24,6 +25,15 @@ class ConsensusTransSetSF : public SHAMapSyncFilter
|
||||
public:
|
||||
using NodeCache = TaggedCache<SHAMapHash, Blob>;
|
||||
|
||||
/**
|
||||
* The size a node's hash-prefixed wire data must reach before gotNode()
|
||||
* tries to parse and resubmit it as a transaction. One byte past the
|
||||
* smallest a hash-prefixed SHAMap leaf can be, which a signed transaction
|
||||
* clears.
|
||||
*/
|
||||
static constexpr std::size_t kMinTxNodeBytesToParse =
|
||||
sizeof(std::uint32_t) + kMinShaMapItemBytes + 1;
|
||||
|
||||
ConsensusTransSetSF(Application& app, NodeCache& nodeCache);
|
||||
|
||||
// Note that the nodeData is overwritten by this call
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
namespace xrpl {
|
||||
|
||||
// A ledger we are trying to acquire
|
||||
class InboundLedger final : public TimeoutCounter,
|
||||
public std::enable_shared_from_this<InboundLedger>,
|
||||
public CountedObject<InboundLedger>
|
||||
class InboundLedger : public TimeoutCounter,
|
||||
public std::enable_shared_from_this<InboundLedger>,
|
||||
public CountedObject<InboundLedger>
|
||||
{
|
||||
public:
|
||||
using ClockType = beast::AbstractClock<std::chrono::steady_clock>;
|
||||
@@ -44,13 +44,29 @@ public:
|
||||
CONSENSUS, // We believe the consensus round requires this ledger
|
||||
};
|
||||
|
||||
/**
|
||||
* How long to wait between retries, and so how long one timeout takes.
|
||||
*/
|
||||
static constexpr std::chrono::milliseconds kRetryInterval{3000};
|
||||
|
||||
/**
|
||||
* @param app The application to run in.
|
||||
* @param hash The ledger to acquire.
|
||||
* @param seq Its sequence, or zero if not known yet.
|
||||
* @param reason Why it is being acquired.
|
||||
* @param clock The clock touch() records against.
|
||||
* @param peerSet Which peers to ask, and how to reach them.
|
||||
* @param retryInterval How long to wait between retries. TimeoutCounter
|
||||
* requires more than 10ms and less than 30s.
|
||||
*/
|
||||
InboundLedger(
|
||||
Application& app,
|
||||
UInt256 const& hash,
|
||||
std::uint32_t seq,
|
||||
Reason reason,
|
||||
ClockType&,
|
||||
std::unique_ptr<PeerSet> peerSet);
|
||||
ClockType& clock,
|
||||
std::unique_ptr<PeerSet> peerSet,
|
||||
std::chrono::milliseconds retryInterval = kRetryInterval);
|
||||
|
||||
~InboundLedger() override;
|
||||
|
||||
@@ -119,15 +135,33 @@ public:
|
||||
return lastAction_;
|
||||
}
|
||||
|
||||
private:
|
||||
protected:
|
||||
// Protected so a test subclass can drive an acquisition through trigger() and done().
|
||||
|
||||
// Why trigger() is being run, which decides how deep a request goes and whether an
|
||||
// aggressive retry applies.
|
||||
enum class TriggerReason { Added, Reply, Timeout };
|
||||
|
||||
/**
|
||||
* Ask for more nodes, or judge what has been collected.
|
||||
*
|
||||
* @param peer The peer to ask, or nullptr to ask everyone being tracked.
|
||||
* @param reason Why the acquisition is being triggered.
|
||||
*/
|
||||
void
|
||||
trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason);
|
||||
|
||||
/**
|
||||
* Settle the acquisition and signal whatever is waiting on it. Runs at most
|
||||
* once. Callers hold mtx_, except trigger(), which releases it first.
|
||||
*/
|
||||
void
|
||||
done();
|
||||
|
||||
private:
|
||||
void
|
||||
filterNodes(std::vector<std::pair<SHAMapNodeID, UInt256>>& nodes, TriggerReason reason);
|
||||
|
||||
void
|
||||
trigger(std::shared_ptr<Peer> const&, TriggerReason);
|
||||
|
||||
std::vector<NeededHashT>
|
||||
getNeededHashes();
|
||||
|
||||
@@ -138,10 +172,7 @@ private:
|
||||
tryDB(node_store::Database& srcDB);
|
||||
|
||||
void
|
||||
done();
|
||||
|
||||
void
|
||||
onTimer(bool progress, ScopedLockType& peerSetLock) override;
|
||||
onTimer(bool progress, ScopedLockType& sl) override;
|
||||
|
||||
std::size_t
|
||||
getPeerCount() const;
|
||||
|
||||
@@ -55,8 +55,6 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
static constexpr auto kPeerCountStart = 5; // Number of peers to start with
|
||||
static constexpr auto kPeerCountAdd = 3; // Number of peers to add on a timeout
|
||||
static constexpr auto kLedgerTimeoutRetriesMax = 6; // how many timeouts before we give up
|
||||
@@ -66,20 +64,18 @@ static constexpr auto kMissingNodesFind = 256; // Number of nodes to find initi
|
||||
static constexpr auto kReqNodesReply = 128; // Number of nodes to request for a reply
|
||||
static constexpr auto kReqNodes = 12; // Number of nodes to request blindly
|
||||
|
||||
// millisecond for each ledger timeout
|
||||
constexpr auto kLedgerAcquireTimeout = 3000ms;
|
||||
|
||||
InboundLedger::InboundLedger(
|
||||
Application& app,
|
||||
UInt256 const& hash,
|
||||
std::uint32_t seq,
|
||||
Reason reason,
|
||||
ClockType& clock,
|
||||
std::unique_ptr<PeerSet> peerSet)
|
||||
std::unique_ptr<PeerSet> peerSet,
|
||||
std::chrono::milliseconds retryInterval)
|
||||
: TimeoutCounter(
|
||||
app,
|
||||
hash,
|
||||
kLedgerAcquireTimeout,
|
||||
retryInterval,
|
||||
{.jobType = JtLedgerData, .jobName = "InboundLedger", .jobLimit = 5},
|
||||
app.getJournal("InboundLedger"))
|
||||
, clock_(clock)
|
||||
|
||||
@@ -98,9 +98,14 @@ protected:
|
||||
|
||||
/**
|
||||
* Hook called from invokeOnTimer().
|
||||
*
|
||||
* @param progress Whether the subtype recorded progress since the
|
||||
* last call.
|
||||
* @param sl Proof mtx_ is held. mtx_ is recursive, so a nested lock taken
|
||||
* inside the call does not release it.
|
||||
*/
|
||||
virtual void
|
||||
onTimer(bool progress, ScopedLockType&) = 0;
|
||||
onTimer(bool progress, ScopedLockType& sl) = 0;
|
||||
|
||||
/**
|
||||
* Return a weak pointer to this.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <xrpl.pb.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
@@ -26,22 +27,18 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// Timeout interval in milliseconds
|
||||
constexpr auto kTxAcquireTimeout = 250ms;
|
||||
|
||||
static constexpr auto kNormTimeouts = 4;
|
||||
static constexpr auto kMaxTimeouts = 20;
|
||||
|
||||
TransactionAcquire::TransactionAcquire(
|
||||
Application& app,
|
||||
UInt256 const& hash,
|
||||
std::unique_ptr<PeerSet> peerSet)
|
||||
std::unique_ptr<PeerSet> peerSet,
|
||||
std::chrono::milliseconds retryInterval)
|
||||
: TimeoutCounter(
|
||||
app,
|
||||
hash,
|
||||
kTxAcquireTimeout,
|
||||
retryInterval,
|
||||
{.jobType = JtTxnData, .jobName = "TxAcq", .jobLimit = {}},
|
||||
app.getJournal("TransactionAcquire"))
|
||||
, peerSet_(std::move(peerSet))
|
||||
@@ -79,7 +76,7 @@ TransactionAcquire::done()
|
||||
}
|
||||
|
||||
void
|
||||
TransactionAcquire::onTimer(bool progress, ScopedLockType& psl)
|
||||
TransactionAcquire::onTimer(bool progress, ScopedLockType&)
|
||||
{
|
||||
if (timeouts_ > kMaxTimeouts)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <xrpl/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/shamap/SHAMapTreeNode.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
@@ -21,14 +22,30 @@ namespace xrpl {
|
||||
|
||||
// VFALCO TODO rename to PeerTxRequest
|
||||
// A transaction set we are trying to acquire
|
||||
class TransactionAcquire final : public TimeoutCounter,
|
||||
public std::enable_shared_from_this<TransactionAcquire>,
|
||||
public CountedObject<TransactionAcquire>
|
||||
class TransactionAcquire : public TimeoutCounter,
|
||||
public std::enable_shared_from_this<TransactionAcquire>,
|
||||
public CountedObject<TransactionAcquire>
|
||||
{
|
||||
public:
|
||||
using pointer = std::shared_ptr<TransactionAcquire>;
|
||||
|
||||
TransactionAcquire(Application& app, UInt256 const& hash, std::unique_ptr<PeerSet> peerSet);
|
||||
/**
|
||||
* How long to wait between retries, and so how long one timeout takes.
|
||||
*/
|
||||
static constexpr std::chrono::milliseconds kRetryInterval{250};
|
||||
|
||||
/**
|
||||
* @param app The application to run in.
|
||||
* @param hash The set to acquire.
|
||||
* @param peerSet Which peers to ask, and how to reach them.
|
||||
* @param retryInterval How long to wait between retries. TimeoutCounter
|
||||
* requires more than 10ms and less than 30s.
|
||||
*/
|
||||
TransactionAcquire(
|
||||
Application& app,
|
||||
UInt256 const& hash,
|
||||
std::unique_ptr<PeerSet> peerSet,
|
||||
std::chrono::milliseconds retryInterval = kRetryInterval);
|
||||
~TransactionAcquire() override = default;
|
||||
|
||||
SHAMapAddNode
|
||||
@@ -42,14 +59,23 @@ public:
|
||||
void
|
||||
stillNeed();
|
||||
|
||||
private:
|
||||
protected:
|
||||
// Kept protected so a test subclass can read the map's state.
|
||||
// Production callers reach a set through InboundTransactions.
|
||||
std::shared_ptr<SHAMap> map_;
|
||||
|
||||
private:
|
||||
bool haveRoot_{false};
|
||||
std::unique_ptr<PeerSet> peerSet_;
|
||||
|
||||
void
|
||||
onTimer(bool progress, ScopedLockType& peerSetLock) override;
|
||||
onTimer(bool progress, ScopedLockType& sl) override;
|
||||
|
||||
/**
|
||||
* Settle the acquired set and hand it on, or report the failure. Call under
|
||||
* mtx_. Runs once per outcome, and a stillNeed() revival gives one object a
|
||||
* second outcome.
|
||||
*/
|
||||
void
|
||||
done();
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
#include <xrpld/rpc/detail/Handler.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
#include <xrpld/rpc/detail/PathRequestManager.h>
|
||||
#include <xrpld/rpc/detail/Pathfinder.h>
|
||||
#include <xrpld/shamap/NodeFamily.h>
|
||||
@@ -1460,20 +1461,25 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
//
|
||||
// Execute start up rpc commands.
|
||||
//
|
||||
for (auto const& cmd : config_->section(Sections::kRpcStartup).lines())
|
||||
auto const& startupCommands = config_->section(Sections::kRpcStartup).lines();
|
||||
for (std::size_t i = 0; i < startupCommands.size(); ++i)
|
||||
{
|
||||
json::Reader jrReader;
|
||||
json::Value jvCommand;
|
||||
|
||||
if (!jrReader.parse(cmd, jvCommand))
|
||||
// A line that does not parse has no field to mask, so it is reported by position and
|
||||
// parser message only, and no command runs for it.
|
||||
if (!jrReader.parse(startupCommands[i], jvCommand))
|
||||
{
|
||||
JLOG(journal_.fatal())
|
||||
<< "Couldn't parse entry in [" << Sections::kRpcStartup << "]: '" << cmd;
|
||||
<< "Couldn't parse entry " << (i + 1) << " in [" << Sections::kRpcStartup
|
||||
<< "]: " << jrReader.getFormattedErrorMessages();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!config_->quiet())
|
||||
{
|
||||
JLOG(journal_.fatal()) << "Startup RPC: " << jvCommand << std::endl;
|
||||
JLOG(journal_.fatal()) << "Startup RPC: " << rpc::loggable(jvCommand) << std::endl;
|
||||
}
|
||||
|
||||
resource::Charge loadType = resource::kFeeReferenceRpc;
|
||||
@@ -1499,7 +1505,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
|
||||
if (!config_->quiet())
|
||||
{
|
||||
JLOG(journal_.fatal()) << "Result: " << jvResult << std::endl;
|
||||
JLOG(journal_.fatal()) << "Result: " << rpc::loggable(jvResult) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3884,37 +3884,32 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
HashSet<InfoSub::pointer> notify;
|
||||
int iProposed = 0;
|
||||
|
||||
std::vector<SubAccountHistoryInfo> accountHistoryNotify;
|
||||
|
||||
{
|
||||
std::scoped_lock const sl(accountLock_);
|
||||
|
||||
if (subRTAccount_.empty())
|
||||
return;
|
||||
|
||||
if (!subAccount_.empty() || !subRTAccount_.empty() || !subAccountHistory_.empty())
|
||||
for (auto const& affectedAccount : tx->getMentionedAccounts())
|
||||
{
|
||||
for (auto const& affectedAccount : tx->getMentionedAccounts())
|
||||
if (auto const simiIt = subRTAccount_.find(affectedAccount);
|
||||
simiIt != subRTAccount_.end())
|
||||
{
|
||||
if (auto const simiIt = subRTAccount_.find(affectedAccount);
|
||||
simiIt != subRTAccount_.end())
|
||||
auto it = simiIt->second.begin();
|
||||
|
||||
while (it != simiIt->second.end())
|
||||
{
|
||||
auto it = simiIt->second.begin();
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
|
||||
while (it != simiIt->second.end())
|
||||
if (p)
|
||||
{
|
||||
InfoSub::pointer const p = it->second.lock();
|
||||
|
||||
if (p)
|
||||
{
|
||||
notify.insert(p);
|
||||
++it;
|
||||
++iProposed;
|
||||
}
|
||||
else
|
||||
{
|
||||
it = simiIt->second.erase(it);
|
||||
}
|
||||
notify.insert(p);
|
||||
++it;
|
||||
++iProposed;
|
||||
}
|
||||
else
|
||||
{
|
||||
it = simiIt->second.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3923,10 +3918,10 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
|
||||
JLOG(journal_.trace()) << "pubProposedAccountTransaction: " << iProposed;
|
||||
|
||||
if (!notify.empty() || !accountHistoryNotify.empty())
|
||||
if (!notify.empty())
|
||||
{
|
||||
// Create two different Json objects, for different API versions
|
||||
MultiApiJson jvObj = transJson(tx, result, false, ledger, std::nullopt);
|
||||
MultiApiJson const jvObj = transJson(tx, result, false, ledger, std::nullopt);
|
||||
|
||||
for (InfoSub::Ref isrListener : notify)
|
||||
{
|
||||
@@ -3934,21 +3929,6 @@ NetworkOPsImp::pubProposedAccountTransaction(
|
||||
isrListener->getApiVersion(), //
|
||||
[&](json::Value const& jv) { isrListener->send(jv, true); });
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
jvObj.isMember(jss::account_history_tx_stream) == MultiApiJson::IsMemberResult::None,
|
||||
"xrpl::NetworkOPs::pubProposedAccountTransaction : "
|
||||
"account_history_tx_stream not set");
|
||||
for (auto& info : accountHistoryNotify)
|
||||
{
|
||||
auto const& index = info.index;
|
||||
if (index->forwardTxIndex == 0 && !index->haveHistorical)
|
||||
jvObj.set(jss::account_history_tx_first, true);
|
||||
jvObj.set(jss::account_history_tx_index, index->forwardTxIndex++);
|
||||
jvObj.visit(
|
||||
info.sink->getApiVersion(), //
|
||||
[&](json::Value const& jv) { info.sink->send(jv, true); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,6 +171,18 @@ public:
|
||||
void
|
||||
onRequest(Session& session);
|
||||
|
||||
/**
|
||||
* Receives one WebSocket frame.
|
||||
*
|
||||
* A frame that exceeds the request size limit, does not parse or is not
|
||||
* an object is answered here with `jsonInvalid` and the frame's `size`;
|
||||
* its body is never echoed, since an unparsed body has no fields to mask.
|
||||
* Any other frame is posted to the job queue and answered from
|
||||
* processSession.
|
||||
*
|
||||
* @param session The WebSocket session the frame arrived on.
|
||||
* @param buffers The frame's bytes.
|
||||
*/
|
||||
void
|
||||
onWSMessage(
|
||||
std::shared_ptr<WSSession> session,
|
||||
@@ -183,15 +195,54 @@ public:
|
||||
onStopped(Server&);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Serves one parsed WebSocket request.
|
||||
*
|
||||
* Closes the connection when its resource balance is past the drop
|
||||
* threshold. Otherwise checks the API version and the `command` and
|
||||
* `method` fields, dispatches through rpc::doCommand, charges the
|
||||
* session's consumer, and shapes the reply as a `response`, echoing the
|
||||
* masked request on an error.
|
||||
*
|
||||
* @param session The session the request arrived on.
|
||||
* @param coro The coroutine the request runs on.
|
||||
* @param jv The parsed request.
|
||||
* @return The reply to send.
|
||||
*/
|
||||
json::Value
|
||||
processSession(
|
||||
std::shared_ptr<WSSession> const& session,
|
||||
std::shared_ptr<JobQueue::Coro> const& coro,
|
||||
json::Value const& jv);
|
||||
|
||||
/**
|
||||
* Serves one HTTP request on a coroutine: hands the body, the client
|
||||
* address and the forwarding headers to processRequest, then completes
|
||||
* or closes the session as its keep-alive header asks.
|
||||
*
|
||||
* @param session The HTTP session the request arrived on.
|
||||
* @param coro The coroutine the request runs on.
|
||||
*/
|
||||
void
|
||||
processSession(std::shared_ptr<Session> const&, std::shared_ptr<JobQueue::Coro> coro);
|
||||
processSession(std::shared_ptr<Session> const& session, std::shared_ptr<JobQueue::Coro> coro);
|
||||
|
||||
/**
|
||||
* Serves one HTTP body: parses it, answers a malformed or unauthorized
|
||||
* request with a plain-text status, serves a `method: "batch"` body entry
|
||||
* by entry, dispatches each request through rpc::doCommand, and writes
|
||||
* the reply with its HTTP status. An error reply echoes the masked
|
||||
* request.
|
||||
*
|
||||
* @param port The port the request arrived on, for its role and limits.
|
||||
* @param request The raw body.
|
||||
* @param remoteIPAddress The client address, for resource accounting and
|
||||
* the role.
|
||||
* @param output Where the reply bytes are written.
|
||||
* @param coro The coroutine the request runs on.
|
||||
* @param forwardedFor The `X-Forwarded-For` header, when the port trusts
|
||||
* a proxy.
|
||||
* @param user The `X-User` header.
|
||||
*/
|
||||
void
|
||||
processRequest(
|
||||
Port const& port,
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Reports whether @p name is one of kCredentialFields.
|
||||
*
|
||||
* An array element's memberName is empty, which no credential is spelled as,
|
||||
* so an element is never a credential itself and is descended into.
|
||||
*
|
||||
* @param name The member name to look up.
|
||||
* @return True if @p name is a credential-bearing field.
|
||||
*/
|
||||
bool
|
||||
isCredential(std::string_view name)
|
||||
{
|
||||
return !name.empty() && std::ranges::find(kCredentialFields, name) != kCredentialFields.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces every credential-bearing field of @p value in place, at every depth.
|
||||
*
|
||||
* @param value The object or array to mask. Any other value is left alone.
|
||||
*/
|
||||
void
|
||||
maskInPlace(json::Value& value)
|
||||
{
|
||||
if (!value.isObject() && !value.isArray())
|
||||
return;
|
||||
|
||||
for (auto it = value.begin(); it != value.end(); ++it)
|
||||
{
|
||||
// A credential is replaced whatever it holds, so it is not descended into.
|
||||
if (isCredential(it.memberName()))
|
||||
{
|
||||
*it = json::Value{"<masked>"};
|
||||
}
|
||||
else
|
||||
{
|
||||
maskInPlace(*it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
hasSecret(json::Value const& value)
|
||||
{
|
||||
if (!value.isObject() && !value.isArray())
|
||||
return false;
|
||||
|
||||
for (auto it = value.begin(); it != value.end(); ++it)
|
||||
{
|
||||
if (isCredential(it.memberName()) || hasSecret(*it))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
json::Value
|
||||
maskSecrets(json::Value const& request)
|
||||
{
|
||||
auto masked = request;
|
||||
maskInPlace(masked);
|
||||
return masked;
|
||||
}
|
||||
|
||||
std::string
|
||||
loggable(json::Value const& value)
|
||||
{
|
||||
// The copy the mask takes is paid only when there is a credential to replace.
|
||||
auto text = hasSecret(value) ? to_string(maskSecrets(value)) : to_string(value);
|
||||
if (text.size() > kMaxLoggedChars)
|
||||
text.resize(kMaxLoggedChars);
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
|
||||
/**
|
||||
* Fields that carry a credential, in a request or in a reply.
|
||||
*
|
||||
* Literals rather than jss entries, since not every one has a jss entry.
|
||||
*
|
||||
* `username` and `password` are `subscribe`'s deprecated spellings of
|
||||
* `url_username` and `url_password`, which is why a name identifying rather
|
||||
* than authenticating is on the list. The `master_` and `validation_` names
|
||||
* come from `wallet_propose` and `validation_create` replies, and a reply is
|
||||
* logged like a request. `master_key` is also the public key that
|
||||
* `validator_info` and the `manifests` and `validations` streams report, and
|
||||
* it is masked with the rest.
|
||||
*/
|
||||
inline constexpr std::array<std::string_view, 16> kCredentialFields{
|
||||
"admin_password",
|
||||
"admin_user",
|
||||
"master_key",
|
||||
"master_seed",
|
||||
"master_seed_hex",
|
||||
"passphrase",
|
||||
"password",
|
||||
"secret",
|
||||
"seed",
|
||||
"seed_hex",
|
||||
"url_password",
|
||||
"url_username",
|
||||
"username",
|
||||
"validation_key",
|
||||
"validation_private_key",
|
||||
"validation_seed",
|
||||
};
|
||||
|
||||
/**
|
||||
* How much of a request or a reply a log line carries.
|
||||
*
|
||||
* A client chooses the size of what it sends, so nothing it cannot choose
|
||||
* bounds the length.
|
||||
*/
|
||||
inline constexpr std::size_t kMaxLoggedChars = 10000;
|
||||
|
||||
/**
|
||||
* Returns a copy of @p request with every credential-bearing field replaced by
|
||||
* a placeholder.
|
||||
*
|
||||
* Apply to every request echoed back to a caller. Masks at every depth, since
|
||||
* the JSON-RPC transport nests a credential inside `params` and a batch nests
|
||||
* one request per entry. Recursion is bounded by json::Reader::kNestLimit.
|
||||
*
|
||||
* @param request The request to mask.
|
||||
* @return The masked copy.
|
||||
*/
|
||||
[[nodiscard]] json::Value
|
||||
maskSecrets(json::Value const& request);
|
||||
|
||||
/**
|
||||
* Reports whether @p value carries a credential-bearing field at any depth.
|
||||
*
|
||||
* Walks objects and arrays without copying and stops at the first field named
|
||||
* in kCredentialFields, so a caller pays for masking only when there is
|
||||
* something to mask. A value with no members carries none.
|
||||
*
|
||||
* @param value The request or reply to inspect.
|
||||
* @return True if a credential-bearing field is present at any depth.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
hasSecret(json::Value const& value);
|
||||
|
||||
/**
|
||||
* Renders @p value for a log line: masked when it carries a credential, then
|
||||
* truncated to kMaxLoggedChars.
|
||||
*
|
||||
* Every site that writes a request or a reply body to the log goes through
|
||||
* this. A value with no credential is serialized as it is, so the copy the
|
||||
* mask takes is paid only when hasSecret() reports one.
|
||||
*
|
||||
* Call it inside the JLOG argument and never before the macro: that argument is
|
||||
* evaluated only when the sink is active, so hoisting the call puts the walk
|
||||
* and the serialization on every request.
|
||||
*
|
||||
* Truncation is by bytes, so a multi-byte character can be split.
|
||||
*
|
||||
* @param value The request or reply to render.
|
||||
* @return The masked, truncated text.
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
loggable(json::Value const& value);
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/rpc/MethodNames.h>
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -23,6 +24,7 @@
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
@@ -614,21 +616,32 @@ private:
|
||||
return rpcError(RpcInvalidParams);
|
||||
}
|
||||
|
||||
// json <command> <json>
|
||||
/**
|
||||
* Parses the `json <command> <json>` form: the second argument is parsed
|
||||
* as the request and the first becomes its `method`. The request is
|
||||
* logged masked once parsed; the unparsed text never is.
|
||||
*
|
||||
* @param jvParams The command name and the JSON text.
|
||||
* @return The request, or `invalidParams` when the text does not parse
|
||||
* to an object.
|
||||
*/
|
||||
json::Value
|
||||
parseJson(json::Value const& jvParams)
|
||||
{
|
||||
json::Reader reader;
|
||||
json::Value jvRequest;
|
||||
|
||||
// The command name cannot carry a credential, so it logs whether the JSON parses or not.
|
||||
JLOG(j_.trace()) << "RPC method: " << jvParams[0u];
|
||||
JLOG(j_.trace()) << "RPC json: " << jvParams[1u];
|
||||
|
||||
if (reader.parse(jvParams[1u].asString(), jvRequest))
|
||||
{
|
||||
if (!jvRequest.isObjectOrNull())
|
||||
return rpcError(RpcInvalidParams);
|
||||
|
||||
// Logged only once parsed, so a signing secret among the members can be masked.
|
||||
JLOG(j_.trace()) << "RPC json: " << rpc::loggable(jvRequest);
|
||||
|
||||
jvRequest[jss::method] = jvParams[0u];
|
||||
|
||||
return jvRequest;
|
||||
@@ -637,6 +650,16 @@ private:
|
||||
return rpcError(RpcInvalidParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether @p jv is a request this client accepts as JSON-RPC 2.0:
|
||||
* an object whose `jsonrpc` is rpc::kJsonRpcVersion and `ripplerpc` is
|
||||
* rpc::kRippleRpcVersion2, carrying an `id` and a `method`, with `params`
|
||||
* absent, null, an array or an object; or a non-empty array of such
|
||||
* objects.
|
||||
*
|
||||
* @param jv The parsed argument.
|
||||
* @return True when the shape is accepted.
|
||||
*/
|
||||
bool
|
||||
isValidJson2(json::Value const& jv)
|
||||
{
|
||||
@@ -651,8 +674,8 @@ private:
|
||||
}
|
||||
if (jv.isObject())
|
||||
{
|
||||
if (jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0" &&
|
||||
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0" &&
|
||||
if (jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == rpc::kJsonRpcVersion &&
|
||||
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == rpc::kRippleRpcVersion2 &&
|
||||
jv.isMember(jss::id) && jv.isMember(jss::method))
|
||||
{
|
||||
return !jv.isMember(jss::params) ||
|
||||
@@ -1010,7 +1033,14 @@ private:
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// ripple_path_find <json> [<ledger>]
|
||||
/**
|
||||
* Parses the `ripple_path_find <json> [<ledger>]` form: the first
|
||||
* argument is parsed as the request and logged masked once parsed, the
|
||||
* optional second names the ledger.
|
||||
*
|
||||
* @param jvParams The JSON text and, optionally, the ledger.
|
||||
* @return The request, or `invalidParams` when the text does not parse.
|
||||
*/
|
||||
json::Value
|
||||
parseRipplePathFind(json::Value const& jvParams)
|
||||
{
|
||||
@@ -1018,10 +1048,12 @@ private:
|
||||
json::Value jvRequest{json::ValueType::Object};
|
||||
bool const bLedger = 2 == jvParams.size();
|
||||
|
||||
JLOG(j_.trace()) << "RPC json: " << jvParams[0u];
|
||||
|
||||
if (reader.parse(jvParams[0u].asString(), jvRequest))
|
||||
{
|
||||
// The JSON is logged only once parsed, so a signing secret among the named members can
|
||||
// be masked. Unparsed it is a bare string with no members to name.
|
||||
JLOG(j_.trace()) << "RPC json: " << rpc::loggable(jvRequest);
|
||||
|
||||
if (bLedger)
|
||||
{
|
||||
jvParseLedger(jvRequest, jvParams[1u].asString());
|
||||
@@ -1750,11 +1782,7 @@ public:
|
||||
json::Value
|
||||
parseCommand(std::string_view strMethod, json::Value const& jvParams, bool allowAnyCommand)
|
||||
{
|
||||
if (auto const stream = j_.trace())
|
||||
{
|
||||
stream << "Method: '" << strMethod << "'";
|
||||
stream << "Params: " << jvParams;
|
||||
}
|
||||
JLOG(j_.trace()) << "Method: '" << strMethod << "'";
|
||||
|
||||
auto const found = std::ranges::lower_bound(kSortedCommands, strMethod, {}, &Command::name);
|
||||
|
||||
@@ -1861,15 +1889,26 @@ struct RPCCallImp
|
||||
"process.");
|
||||
}
|
||||
|
||||
// Parse reply
|
||||
JLOG(j.debug()) << "RPC reply: " << strData << std::endl;
|
||||
// Parse reply. A plain text body is a rejection, which carries no field a credential
|
||||
// could sit in, so it is logged as it is, capped at kMaxLoggedChars.
|
||||
if (strData.starts_with("Unable to parse request") ||
|
||||
strData.starts_with(jss::invalid_API_version.cStr()))
|
||||
{
|
||||
JLOG(j.debug()) << "RPC reply: "
|
||||
<< std::string_view{strData}.substr(0, rpc::kMaxLoggedChars);
|
||||
Throw<RequestNotParsable>(strData);
|
||||
}
|
||||
json::Reader reader;
|
||||
json::Value jvReply;
|
||||
if (!reader.parse(strData, jvReply))
|
||||
{
|
||||
JLOG(j.debug()) << "RPC reply: "
|
||||
<< std::string_view{strData}.substr(0, rpc::kMaxLoggedChars);
|
||||
Throw<std::runtime_error>("couldn't parse reply from server");
|
||||
}
|
||||
// Logged once parsed, masked: `validation_create` and `wallet_propose` answer with a
|
||||
// private key, which the fallback scrubber in Log.cpp does not know by every name.
|
||||
JLOG(j.debug()) << "RPC reply: " << rpc::loggable(jvReply);
|
||||
|
||||
if (!jvReply)
|
||||
Throw<std::runtime_error>("expected reply to have result, error and id properties");
|
||||
@@ -1911,7 +1950,16 @@ commandLineMethodNames()
|
||||
return RPCParser::methodNames();
|
||||
}
|
||||
|
||||
// Used internally by rpcClient.
|
||||
/**
|
||||
* Translates command line arguments into the request the client sends.
|
||||
*
|
||||
* @param args The method name followed by its positional arguments.
|
||||
* @param retParams Receives the arguments as `method` and `params`, the form
|
||||
* `rpc` echoes on an error.
|
||||
* @param apiVersion The `api_version` stamped on each request naming none.
|
||||
* @param j The journal the built request is logged to, masked.
|
||||
* @return The request, an array of requests, or the parser's error object.
|
||||
*/
|
||||
json::Value
|
||||
rpcCmdToJson(
|
||||
std::vector<std::string> const& args,
|
||||
@@ -1951,12 +1999,27 @@ rpcCmdToJson(
|
||||
std::for_each(jvRequest.begin(), jvRequest.end(), insertApiVersion);
|
||||
}
|
||||
|
||||
JLOG(j.trace()) << "RPC Request: " << jvRequest << std::endl;
|
||||
// `sign` and `submit` put the signing secret in the request the command line built.
|
||||
JLOG(j.trace()) << "RPC Request: " << rpc::loggable(jvRequest) << std::endl;
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Runs one command line command against the configured server.
|
||||
*
|
||||
* Builds the request, adds the admin credentials from the config, sends it
|
||||
* over HTTP and unwraps the result. On an error the output also carries the
|
||||
* invocation as `rpc` and the request as `request_sent`, masked.
|
||||
*
|
||||
* @param args The method name followed by its arguments.
|
||||
* @param config The client configuration naming the server and credentials.
|
||||
* @param logs The log manager.
|
||||
* @param apiVersion The `api_version` to request.
|
||||
* @param headers Extra HTTP headers to send.
|
||||
* @return The exit code and the output to print.
|
||||
*/
|
||||
std::pair<int, json::Value>
|
||||
rpcClient(
|
||||
std::vector<std::string> const& args,
|
||||
@@ -2068,7 +2131,9 @@ rpcClient(
|
||||
if (jvOutput.isMember(jss::error))
|
||||
{
|
||||
jvOutput["rpc"] = jvRpc; // How the command was seen as method + params.
|
||||
jvOutput["request_sent"] = jvRequest; // How the command was translated.
|
||||
// Carries the config's `admin_password` and prints to stdout, so it is masked.
|
||||
// `jvRpc` above is positional, so masking by member name does not reach it.
|
||||
jvOutput["request_sent"] = rpc::maskSecrets(jvRequest);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/Handler.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -136,7 +137,7 @@ fillHandler(JsonContext& context, Handler const*& result)
|
||||
: context.params[jss::method].asString();
|
||||
|
||||
JLOG(context.j.trace()) << "COMMAND:" << strCommand;
|
||||
JLOG(context.j.trace()) << "REQUEST:" << context.params;
|
||||
JLOG(context.j.trace()) << "REQUEST:" << loggable(context.params);
|
||||
auto handler = getHandler(context.apiVersion, context.app.config().betaRpcApi, strCommand);
|
||||
|
||||
if (handler == nullptr)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpld/rpc/RPCSub.h>
|
||||
|
||||
#include <xrpld/rpc/RPCCall.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
@@ -87,7 +88,7 @@ public:
|
||||
std::scoped_lock const sl(lock_);
|
||||
|
||||
auto const jm = broadcast ? j_.debug() : j_.info();
|
||||
JLOG(jm) << "rpc_call::fromNetwork push: " << jvObj;
|
||||
JLOG(jm) << "rpc_call::fromNetwork push: " << rpc::loggable(jvObj);
|
||||
|
||||
deque_.emplace_back(seq_++, jvObj);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/rpc/RPCHandler.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
#include <xrpld/rpc/detail/WSInfoSub.h>
|
||||
|
||||
@@ -60,7 +61,9 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -340,10 +343,13 @@ ServerHandler::onWSMessage(
|
||||
auto const size = boost::asio::buffer_size(buffers);
|
||||
if (size > rpc::tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject())
|
||||
{
|
||||
// An unparsed body cannot be masked field-wise, so its size goes instead of its content.
|
||||
// Clamped rather than narrowed: json has no integer wider than 32 bits.
|
||||
json::Value jvResult(json::ValueType::Object);
|
||||
jvResult[jss::type] = jss::error;
|
||||
jvResult[jss::error] = "jsonInvalid";
|
||||
jvResult[jss::value] = buffersToString(buffers);
|
||||
jvResult[jss::size] =
|
||||
json::UInt(std::min<std::size_t>(size, std::numeric_limits<json::UInt>::max()));
|
||||
boost::beast::multi_buffer sb;
|
||||
json::stream(jvResult, [&sb](auto const p, auto const n) {
|
||||
sb.commit(boost::asio::buffer_copy(sb.prepare(n), boost::asio::buffer(p, n)));
|
||||
@@ -354,7 +360,7 @@ ServerHandler::onWSMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
JLOG(journal_.trace()) << "Websocket received '" << jv << "'";
|
||||
JLOG(journal_.trace()) << "Websocket received '" << rpc::loggable(jv) << "'";
|
||||
|
||||
auto const postResult = jobQueue_.postCoro(
|
||||
JtClientWebsocket,
|
||||
@@ -392,22 +398,35 @@ ServerHandler::onStopped(Server&)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Logs how long a request took.
|
||||
*
|
||||
* A slow request is reported at warn from one second and at error from ten,
|
||||
* with the duration only. The request is logged at debug, since it is client
|
||||
* text.
|
||||
*
|
||||
* @param request The request the duration belongs to.
|
||||
* @param duration How long processing it took.
|
||||
* @param journal Where the line is written.
|
||||
*/
|
||||
template <class T>
|
||||
void
|
||||
logDuration(json::Value const& request, T const& duration, beast::Journal& journal)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
auto const level = [&] {
|
||||
if (duration >= 10s)
|
||||
return journal.error();
|
||||
if (duration >= 1s)
|
||||
return journal.warn();
|
||||
return journal.debug();
|
||||
}();
|
||||
auto const micros = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
|
||||
|
||||
JLOG(level) << "RPC request processing duration = "
|
||||
<< std::chrono::duration_cast<std::chrono::microseconds>(duration).count()
|
||||
<< " microseconds. request = " << request;
|
||||
if (duration >= 1s)
|
||||
{
|
||||
auto const slow = duration >= 10s ? journal.error() : journal.warn();
|
||||
JLOG(slow) << "RPC request processing duration = " << micros << " microseconds.";
|
||||
}
|
||||
|
||||
if (auto const stream = journal.debug())
|
||||
{
|
||||
stream << "RPC request processing duration = " << micros
|
||||
<< " microseconds. request = " << rpc::loggable(request);
|
||||
}
|
||||
}
|
||||
|
||||
json::Value
|
||||
@@ -442,7 +461,7 @@ ServerHandler::processSession(
|
||||
jr[jss::status] = jss::error;
|
||||
jr[jss::error] = apiVersion == rpc::kApiInvalidVersion ? jss::invalid_API_version
|
||||
: jss::missingCommand;
|
||||
jr[jss::request] = jv;
|
||||
jr[jss::request] = rpc::maskSecrets(jv);
|
||||
if (jv.isMember(jss::id))
|
||||
jr[jss::id] = jv[jss::id];
|
||||
if (jv.isMember(jss::jsonrpc))
|
||||
@@ -498,11 +517,9 @@ ServerHandler::processSession(
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
jr[jss::result] = rpc::makeError(RpcInternal);
|
||||
JLOG(journal_.error()) << "Exception while processing WS: " << ex.what() << "\n"
|
||||
<< "Input JSON: " << json::Compact{json::Value{jv}};
|
||||
// LCOV_EXCL_STOP
|
||||
<< "Input JSON: " << rpc::loggable(jv);
|
||||
}
|
||||
|
||||
is->getConsumer().charge(loadType);
|
||||
@@ -519,21 +536,7 @@ ServerHandler::processSession(
|
||||
jr = jr[jss::result];
|
||||
jr[jss::status] = jss::error;
|
||||
|
||||
auto rq = jv;
|
||||
|
||||
if (rq.isObject())
|
||||
{
|
||||
if (rq.isMember(jss::passphrase.cStr()))
|
||||
rq[jss::passphrase.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::secret.cStr()))
|
||||
rq[jss::secret.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::seed.cStr()))
|
||||
rq[jss::seed.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::seed_hex.cStr()))
|
||||
rq[jss::seed_hex.cStr()] = "<masked>";
|
||||
}
|
||||
|
||||
jr[jss::request] = rq;
|
||||
jr[jss::request] = rpc::maskSecrets(jv);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -599,6 +602,31 @@ constexpr json::Int kServerOverloaded = -32604;
|
||||
constexpr json::Int kForbidden = -32605;
|
||||
constexpr json::Int kWrongVersion = -32606;
|
||||
|
||||
/**
|
||||
* The HTTP status the `ripplerpc: "3.0"` envelope reports for @p code.
|
||||
*
|
||||
* The codes below answer 200, which is what a client calling `account_info`
|
||||
* this way already reads. The list is closed: a code belongs on it only if it
|
||||
* already answered a coded reply at this status.
|
||||
*
|
||||
* @param code The error code the reply reports.
|
||||
* @return The HTTP status the reply is sent with.
|
||||
*/
|
||||
static int
|
||||
legacyHttpStatus(ErrorCodeI code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case RpcActMalformed:
|
||||
case RpcActNotFound:
|
||||
case RpcAlreadyMultisig:
|
||||
case RpcAlreadySingleSig:
|
||||
return 200;
|
||||
default:
|
||||
return rpc::errorCodeHttpStatus(code);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::processRequest(
|
||||
Port const& port,
|
||||
@@ -628,7 +656,9 @@ ServerHandler::processRequest(
|
||||
|
||||
bool batch = false;
|
||||
unsigned size = 1;
|
||||
if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == "batch")
|
||||
// Spelled as a view so the name is compared in place. A bare literal would reach the
|
||||
// Value-to-Value comparison instead, which builds a Value from it, allocating per request.
|
||||
if (jsonOrig.isMember(jss::method) && jsonOrig[jss::method] == std::string_view{"batch"})
|
||||
{
|
||||
batch = true;
|
||||
if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray())
|
||||
@@ -648,7 +678,7 @@ ServerHandler::processRequest(
|
||||
if (!jsonRPC.isObject())
|
||||
{
|
||||
json::Value r(json::ValueType::Object);
|
||||
r[jss::request] = jsonRPC;
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "Method not found");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -676,7 +706,7 @@ ServerHandler::processRequest(
|
||||
return;
|
||||
}
|
||||
json::Value r(json::ValueType::Object);
|
||||
r[jss::request] = jsonRPC;
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kWrongVersion, jss::invalid_API_version.cStr());
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -718,7 +748,7 @@ ServerHandler::processRequest(
|
||||
httpReply(503, "Server is overloaded", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kServerOverloaded, "Server is overloaded");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -733,7 +763,7 @@ ServerHandler::processRequest(
|
||||
httpReply(403, "Forbidden", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kForbidden, "Forbidden");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -747,7 +777,7 @@ ServerHandler::processRequest(
|
||||
httpReply(400, "Null method", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "Null method");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -762,7 +792,7 @@ ServerHandler::processRequest(
|
||||
httpReply(400, "method is not string", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "method is not string");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -777,7 +807,7 @@ ServerHandler::processRequest(
|
||||
httpReply(400, "method is empty", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "method is empty");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -819,7 +849,7 @@ ServerHandler::processRequest(
|
||||
params = jsonRPC;
|
||||
}
|
||||
|
||||
std::string ripplerpc = "1.0";
|
||||
std::string ripplerpc{rpc::kRippleRpcVersion1};
|
||||
if (params.isMember(jss::ripplerpc))
|
||||
{
|
||||
if (!params[jss::ripplerpc].isString())
|
||||
@@ -831,7 +861,7 @@ ServerHandler::processRequest(
|
||||
return;
|
||||
}
|
||||
|
||||
json::Value r = jsonRPC;
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "ripplerpc is not a string");
|
||||
reply.append(r);
|
||||
continue;
|
||||
@@ -849,11 +879,11 @@ ServerHandler::processRequest(
|
||||
user.remove_suffix(user.size());
|
||||
}
|
||||
|
||||
JLOG(journal_.debug()) << "Query: " << strMethod << params;
|
||||
JLOG(journal_.debug()) << "Query: " << strMethod << rpc::loggable(params);
|
||||
|
||||
// Provide the JSON-RPC method as the field "command" in the request.
|
||||
params[jss::command] = strMethod;
|
||||
JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << params;
|
||||
JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << rpc::loggable(params);
|
||||
|
||||
resource::Charge loadType = resource::kFeeReferenceRpc;
|
||||
|
||||
@@ -883,12 +913,9 @@ ServerHandler::processRequest(
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
result = rpc::makeError(RpcInternal);
|
||||
JLOG(journal_.error())
|
||||
<< "Internal error : " << ex.what()
|
||||
<< " when processing request: " << json::Compact{json::Value{params}};
|
||||
// LCOV_EXCL_STOP
|
||||
JLOG(journal_.error()) << "Internal error : " << ex.what()
|
||||
<< " when processing request: " << rpc::loggable(params);
|
||||
}
|
||||
|
||||
auto const end = std::chrono::system_clock::now();
|
||||
@@ -900,7 +927,7 @@ ServerHandler::processRequest(
|
||||
result[jss::warning] = jss::load;
|
||||
|
||||
json::Value r(json::ValueType::Object);
|
||||
if (ripplerpc >= "2.0")
|
||||
if (ripplerpc >= rpc::kRippleRpcVersion2)
|
||||
{
|
||||
if (result.isMember(jss::error))
|
||||
{
|
||||
@@ -924,22 +951,8 @@ ServerHandler::processRequest(
|
||||
// received.
|
||||
if (result.isMember(jss::error))
|
||||
{
|
||||
auto rq = params;
|
||||
|
||||
if (rq.isObject())
|
||||
{ // But mask potentially sensitive information.
|
||||
if (rq.isMember(jss::passphrase.cStr()))
|
||||
rq[jss::passphrase.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::secret.cStr()))
|
||||
rq[jss::secret.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::seed.cStr()))
|
||||
rq[jss::seed.cStr()] = "<masked>";
|
||||
if (rq.isMember(jss::seed_hex.cStr()))
|
||||
rq[jss::seed_hex.cStr()] = "<masked>";
|
||||
}
|
||||
|
||||
result[jss::status] = jss::error;
|
||||
result[jss::request] = rq;
|
||||
result[jss::request] = rpc::maskSecrets(params);
|
||||
|
||||
JLOG(journal_.debug())
|
||||
<< "rpcError: " << result[jss::error] << ": " << result[jss::error_message];
|
||||
@@ -982,14 +995,14 @@ ServerHandler::processRequest(
|
||||
// This feature is enabled with ripplerpc version 3.0 and above.
|
||||
// Before ripplerpc version 3.0 always return 200.
|
||||
if (reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc].isString() &&
|
||||
reply[jss::ripplerpc].asString() >= "3.0")
|
||||
reply[jss::ripplerpc].asString() >= rpc::kRippleRpcVersion3)
|
||||
{
|
||||
// If there's an error_code, use that to determine the HTTP Status.
|
||||
if (reply.isMember(jss::error) && reply[jss::error].isMember(jss::error_code) &&
|
||||
reply[jss::error][jss::error_code].isInt())
|
||||
{
|
||||
int const errCode = reply[jss::error][jss::error_code].asInt();
|
||||
return rpc::errorCodeHttpStatus(static_cast<ErrorCodeI>(errCode));
|
||||
return legacyHttpStatus(static_cast<ErrorCodeI>(errCode));
|
||||
}
|
||||
}
|
||||
// Return OK.
|
||||
@@ -1004,21 +1017,22 @@ ServerHandler::processRequest(
|
||||
++rpcRequests_;
|
||||
rpcSize_.notify(beast::insight::Event::value_type{response.size()});
|
||||
|
||||
response += '\n';
|
||||
|
||||
// The serialized reply is in hand, so it is logged as built; the masked copy and its second
|
||||
// serialization are paid for only when a credential has to be replaced.
|
||||
if (auto const stream = journal_.debug())
|
||||
{
|
||||
static int const kMaxSize = 10000;
|
||||
if (response.size() <= kMaxSize)
|
||||
if (rpc::hasSecret(reply))
|
||||
{
|
||||
stream << "Reply: " << response;
|
||||
stream << "Reply: " << rpc::loggable(reply);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream << "Reply: " << response.substr(0, kMaxSize);
|
||||
stream << "Reply: " << std::string_view{response}.substr(0, rpc::kMaxLoggedChars);
|
||||
}
|
||||
}
|
||||
|
||||
response += '\n';
|
||||
|
||||
httpReply(httpStatus, response, output, rpcJ);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/AssetCache.h>
|
||||
#include <xrpld/rpc/detail/LegacyPathFind.h>
|
||||
#include <xrpld/rpc/detail/MaskSecrets.h>
|
||||
#include <xrpld/rpc/detail/Pathfinder.h>
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
@@ -1018,7 +1019,8 @@ transactionSign(
|
||||
// could change the signing prefix of an alternate signature field.
|
||||
std::shared_ptr<ReadView const> const ledger = app.getOpenLedger().current();
|
||||
auto const j = app.getJournal("RPCHandler");
|
||||
JLOG(j.debug()) << "transactionSign: " << jvRequest;
|
||||
// The next statement reads the signing secret out of this same request.
|
||||
JLOG(j.debug()) << "transactionSign: " << loggable(jvRequest);
|
||||
|
||||
// Add and amend fields based on the transaction type.
|
||||
SigningForParams signForParams;
|
||||
@@ -1055,7 +1057,7 @@ transactionSubmit(
|
||||
|
||||
auto const& ledger = app.getOpenLedger().current();
|
||||
auto const j = app.getJournal("RPCHandler");
|
||||
JLOG(j.debug()) << "transactionSubmit: " << jvRequest;
|
||||
JLOG(j.debug()) << "transactionSubmit: " << loggable(jvRequest);
|
||||
|
||||
// Add and amend fields based on the transaction type.
|
||||
SigningForParams signForParams;
|
||||
@@ -1177,7 +1179,7 @@ transactionSignFor(
|
||||
{
|
||||
auto const& ledger = app.getOpenLedger().current();
|
||||
auto const j = app.getJournal("RPCHandler");
|
||||
JLOG(j.debug()) << "transactionSignFor: " << jvRequest;
|
||||
JLOG(j.debug()) << "transactionSignFor: " << loggable(jvRequest);
|
||||
|
||||
// Verify presence of the signer's account field.
|
||||
char const accountField[] = "account";
|
||||
@@ -1301,7 +1303,7 @@ transactionSubmitMultiSigned(
|
||||
{
|
||||
auto const& ledger = app.getOpenLedger().current();
|
||||
auto const j = app.getJournal("RPCHandler");
|
||||
JLOG(j.debug()) << "transactionSubmitMultiSigned: " << jvRequest;
|
||||
JLOG(j.debug()) << "transactionSubmitMultiSigned: " << loggable(jvRequest);
|
||||
|
||||
// When multi-signing, the "Sequence" and "SigningPubKey" fields must
|
||||
// be passed in by the caller.
|
||||
|
||||
@@ -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