mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-20 03:35:55 +00:00
Add error code extension mechanism and use malformed currency code (#396)
Fixes #275
This commit is contained in:
@@ -6,7 +6,7 @@ exec 1>&2
|
||||
find src unittests -type f \( -name '*.cpp' -o -name '*.h' -o -name '*.ipp' \) -print0 | xargs -0 clang-format -i
|
||||
|
||||
# check how many lines differ
|
||||
lines=$(git diff | wc -l)
|
||||
lines=$(git diff src unittests | wc -l)
|
||||
|
||||
# check if there is any updated files
|
||||
if [ "$lines" != "0" ]; then
|
||||
|
||||
@@ -61,6 +61,7 @@ target_sources(clio PRIVATE
|
||||
## Subscriptions
|
||||
src/subscriptions/SubscriptionManager.cpp
|
||||
## RPC
|
||||
src/rpc/Errors.cpp
|
||||
src/rpc/RPC.cpp
|
||||
src/rpc/RPCHelpers.cpp
|
||||
src/rpc/Counters.cpp
|
||||
@@ -107,7 +108,8 @@ add_executable(clio_server src/main/main.cpp)
|
||||
target_link_libraries(clio_server PUBLIC clio)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_executable(clio_tests
|
||||
add_executable(clio_tests
|
||||
unittests/RPCErrors.cpp
|
||||
unittests/config.cpp
|
||||
unittests/main.cpp)
|
||||
include(CMake/deps/gtest.cmake)
|
||||
|
||||
135
src/rpc/Errors.cpp
Normal file
135
src/rpc/Errors.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
#include <rpc/Errors.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
template <class... Ts>
|
||||
struct overloadSet : Ts...
|
||||
{
|
||||
using Ts::operator()...;
|
||||
};
|
||||
|
||||
// explicit deduction guide (not needed as of C++20, but clang be clang)
|
||||
template <class... Ts>
|
||||
overloadSet(Ts...) -> overloadSet<Ts...>;
|
||||
} // namespace
|
||||
|
||||
namespace RPC {
|
||||
|
||||
WarningInfo const&
|
||||
getWarningInfo(WarningCode code)
|
||||
{
|
||||
constexpr static WarningInfo infos[]{
|
||||
{warnUNKNOWN, "Unknown warning"},
|
||||
{warnRPC_CLIO,
|
||||
"This is a clio server. clio only serves validated data. If you "
|
||||
"want to talk to rippled, include 'ledger_index':'current' in your "
|
||||
"request"},
|
||||
{warnRPC_OUTDATED, "This server may be out of date"},
|
||||
{warnRPC_RATE_LIMIT, "You are about to be rate limited"}};
|
||||
|
||||
auto matchByCode = [code](auto const& info) { return info.code == code; };
|
||||
if (auto it = find_if(begin(infos), end(infos), matchByCode);
|
||||
it != end(infos))
|
||||
return *it;
|
||||
|
||||
throw(out_of_range("Invalid WarningCode"));
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
makeWarning(WarningCode code)
|
||||
{
|
||||
boost::json::object json;
|
||||
auto const& info = getWarningInfo(code);
|
||||
json["id"] = code;
|
||||
json["message"] = static_cast<string>(info.message);
|
||||
return json;
|
||||
}
|
||||
|
||||
ClioErrorInfo const&
|
||||
getErrorInfo(ClioError code)
|
||||
{
|
||||
constexpr static ClioErrorInfo infos[]{
|
||||
{ClioError::rpcMALFORMED_CURRENCY,
|
||||
"malformedCurrency",
|
||||
"Malformed currency."},
|
||||
};
|
||||
|
||||
auto matchByCode = [code](auto const& info) { return info.code == code; };
|
||||
if (auto it = find_if(begin(infos), end(infos), matchByCode);
|
||||
it != end(infos))
|
||||
return *it;
|
||||
|
||||
throw(out_of_range("Invalid error code"));
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
makeError(
|
||||
RippledError err,
|
||||
optional<string_view> customError,
|
||||
optional<string_view> customMessage)
|
||||
{
|
||||
boost::json::object json;
|
||||
auto const& info = ripple::RPC::get_error_info(err);
|
||||
|
||||
json["error"] = customError.value_or(info.token.c_str()).data();
|
||||
json["error_code"] = static_cast<uint32_t>(err);
|
||||
json["error_message"] = customMessage.value_or(info.message.c_str()).data();
|
||||
json["status"] = "error";
|
||||
json["type"] = "response";
|
||||
return json;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
makeError(
|
||||
ClioError err,
|
||||
optional<string_view> customError,
|
||||
optional<string_view> customMessage)
|
||||
{
|
||||
boost::json::object json;
|
||||
auto const& info = getErrorInfo(err);
|
||||
|
||||
json["error"] = customError.value_or(info.error).data();
|
||||
json["error_code"] = static_cast<uint32_t>(info.code);
|
||||
json["error_message"] = customMessage.value_or(info.message).data();
|
||||
json["status"] = "error";
|
||||
json["type"] = "response";
|
||||
return json;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
makeError(Status const& status)
|
||||
{
|
||||
auto wrapOptional = [](string_view const& str) {
|
||||
return str.empty() ? nullopt : make_optional(str);
|
||||
};
|
||||
|
||||
return visit(
|
||||
overloadSet{
|
||||
[&status, &wrapOptional](RippledError err) {
|
||||
if (err == ripple::rpcUNKNOWN)
|
||||
{
|
||||
return boost::json::object{
|
||||
{"error", status.message},
|
||||
{"type", "response"},
|
||||
{"status", "error"}};
|
||||
}
|
||||
|
||||
return makeError(
|
||||
err,
|
||||
wrapOptional(status.error),
|
||||
wrapOptional(status.message));
|
||||
},
|
||||
[&status, &wrapOptional](ClioError err) {
|
||||
return makeError(
|
||||
err,
|
||||
wrapOptional(status.error),
|
||||
wrapOptional(status.message));
|
||||
},
|
||||
},
|
||||
status.code);
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
206
src/rpc/Errors.h
Normal file
206
src/rpc/Errors.h
Normal file
@@ -0,0 +1,206 @@
|
||||
#ifndef REPORTING_RPC_ERRORS_H_INCLUDED
|
||||
#define REPORTING_RPC_ERRORS_H_INCLUDED
|
||||
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
namespace RPC {
|
||||
|
||||
/**
|
||||
* @brief Custom clio RPC Errors.
|
||||
*/
|
||||
enum class ClioError {
|
||||
rpcMALFORMED_CURRENCY = 5000,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Holds info about a particular @ref ClioError.
|
||||
*/
|
||||
struct ClioErrorInfo
|
||||
{
|
||||
ClioError const code;
|
||||
std::string_view const error;
|
||||
std::string_view const message;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Clio uses compatible Rippled error codes for most RPC errors.
|
||||
*/
|
||||
using RippledError = ripple::error_code_i;
|
||||
|
||||
/**
|
||||
* @brief Clio operates on a combination of Rippled and Custom Clio error codes.
|
||||
*
|
||||
* @see RippledError For rippled error codes
|
||||
* @see ClioError For custom clio error codes
|
||||
*/
|
||||
using CombinedError = std::variant<RippledError, ClioError>;
|
||||
|
||||
/**
|
||||
* @brief A status returned from any RPC handler.
|
||||
*/
|
||||
struct Status
|
||||
{
|
||||
CombinedError code = RippledError::rpcSUCCESS;
|
||||
std::string error = "";
|
||||
std::string message = "";
|
||||
|
||||
Status() = default;
|
||||
/* implicit */ Status(CombinedError code) : code(code){};
|
||||
|
||||
// HACK. Some rippled handlers explicitly specify errors.
|
||||
// This means that we have to be able to duplicate this
|
||||
// functionality.
|
||||
explicit Status(std::string const& message)
|
||||
: code(ripple::rpcUNKNOWN), message(message)
|
||||
{
|
||||
}
|
||||
|
||||
Status(CombinedError code, std::string message)
|
||||
: code(code), message(message)
|
||||
{
|
||||
}
|
||||
|
||||
Status(CombinedError code, std::string error, std::string message)
|
||||
: code(code), error(error), message(message)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns true if the Status is *not* OK.
|
||||
*/
|
||||
operator bool() const
|
||||
{
|
||||
if (auto err = std::get_if<RippledError>(&code))
|
||||
return *err != RippledError::rpcSUCCESS;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Warning codes that can be returned by clio.
|
||||
*/
|
||||
enum WarningCode {
|
||||
warnUNKNOWN = -1,
|
||||
warnRPC_CLIO = 2001,
|
||||
warnRPC_OUTDATED = 2002,
|
||||
warnRPC_RATE_LIMIT = 2003
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Holds information about a clio warning.
|
||||
*/
|
||||
struct WarningInfo
|
||||
{
|
||||
constexpr WarningInfo() = default;
|
||||
constexpr WarningInfo(WarningCode code, char const* message)
|
||||
: code(code), message(message)
|
||||
{
|
||||
}
|
||||
|
||||
WarningCode code = warnUNKNOWN;
|
||||
std::string_view const message = "unknown warning";
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invalid parameters error.
|
||||
*/
|
||||
class InvalidParamsError : public std::exception
|
||||
{
|
||||
std::string msg;
|
||||
|
||||
public:
|
||||
explicit InvalidParamsError(std::string const& msg) : msg(msg)
|
||||
{
|
||||
}
|
||||
|
||||
const char*
|
||||
what() const throw() override
|
||||
{
|
||||
return msg.c_str();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Account not found error.
|
||||
*/
|
||||
class AccountNotFoundError : public std::exception
|
||||
{
|
||||
std::string account;
|
||||
|
||||
public:
|
||||
explicit AccountNotFoundError(std::string const& acct) : account(acct)
|
||||
{
|
||||
}
|
||||
const char*
|
||||
what() const throw() override
|
||||
{
|
||||
return account.c_str();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A globally available @ref Status that represents a successful state
|
||||
*/
|
||||
static Status OK;
|
||||
|
||||
/**
|
||||
* @brief Get the warning info object from a warning code.
|
||||
*
|
||||
* @param code The warning code
|
||||
* @return WarningInfo const& A reference to the static warning info
|
||||
*/
|
||||
WarningInfo const&
|
||||
getWarningInfo(WarningCode code);
|
||||
|
||||
/**
|
||||
* @brief Generate JSON from a warning code.
|
||||
*
|
||||
* @param code The @ref WarningCode
|
||||
* @return boost::json::object The JSON output
|
||||
*/
|
||||
boost::json::object
|
||||
makeWarning(WarningCode code);
|
||||
|
||||
/**
|
||||
* @brief Generate JSON from a @ref Status.
|
||||
*
|
||||
* @param status The @ref Status
|
||||
* @return boost::json::object The JSON output
|
||||
*/
|
||||
boost::json::object
|
||||
makeError(Status const& status);
|
||||
|
||||
/**
|
||||
* @brief Generate JSON from a @ref RippledError.
|
||||
*
|
||||
* @param status The rippled @ref RippledError
|
||||
* @return boost::json::object The JSON output
|
||||
*/
|
||||
boost::json::object
|
||||
makeError(
|
||||
RippledError err,
|
||||
std::optional<std::string_view> customError = std::nullopt,
|
||||
std::optional<std::string_view> customMessage = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Generate JSON from a @ref ClioError.
|
||||
*
|
||||
* @param status The clio's custom @ref ClioError
|
||||
* @return boost::json::object The JSON output
|
||||
*/
|
||||
boost::json::object
|
||||
makeError(
|
||||
ClioError err,
|
||||
std::optional<std::string_view> customError = std::nullopt,
|
||||
std::optional<std::string_view> customMessage = std::nullopt);
|
||||
|
||||
} // namespace RPC
|
||||
|
||||
#endif // REPORTING_RPC_ERRORS_H_INCLUDED
|
||||
188
src/rpc/RPC.cpp
188
src/rpc/RPC.cpp
@@ -8,22 +8,24 @@
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace RPC {
|
||||
|
||||
Context::Context(
|
||||
boost::asio::yield_context& yield_,
|
||||
std::string const& command_,
|
||||
std::uint32_t version_,
|
||||
string const& command_,
|
||||
uint32_t version_,
|
||||
boost::json::object const& params_,
|
||||
std::shared_ptr<BackendInterface const> const& backend_,
|
||||
std::shared_ptr<SubscriptionManager> const& subscriptions_,
|
||||
std::shared_ptr<ETLLoadBalancer> const& balancer_,
|
||||
std::shared_ptr<ReportingETL const> const& etl_,
|
||||
std::shared_ptr<WsBase> const& session_,
|
||||
shared_ptr<BackendInterface const> const& backend_,
|
||||
shared_ptr<SubscriptionManager> const& subscriptions_,
|
||||
shared_ptr<ETLLoadBalancer> const& balancer_,
|
||||
shared_ptr<ReportingETL const> const& etl_,
|
||||
shared_ptr<WsBase> const& session_,
|
||||
util::TagDecoratorFactory const& tagFactory_,
|
||||
Backend::LedgerRange const& range_,
|
||||
Counters& counters_,
|
||||
std::string const& clientIp_)
|
||||
string const& clientIp_)
|
||||
: Taggable(tagFactory_)
|
||||
, yield(yield_)
|
||||
, method(command_)
|
||||
@@ -41,19 +43,19 @@ Context::Context(
|
||||
BOOST_LOG_TRIVIAL(debug) << tag() << "new Context created";
|
||||
}
|
||||
|
||||
std::optional<Context>
|
||||
optional<Context>
|
||||
make_WsContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
std::shared_ptr<BackendInterface const> const& backend,
|
||||
std::shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
std::shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
std::shared_ptr<ReportingETL const> const& etl,
|
||||
std::shared_ptr<WsBase> const& session,
|
||||
shared_ptr<BackendInterface const> const& backend,
|
||||
shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
shared_ptr<ReportingETL const> const& etl,
|
||||
shared_ptr<WsBase> const& session,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
Counters& counters,
|
||||
std::string const& clientIp)
|
||||
string const& clientIp)
|
||||
{
|
||||
boost::json::value commandValue = nullptr;
|
||||
if (!request.contains("command") && request.contains("method"))
|
||||
@@ -64,9 +66,9 @@ make_WsContext(
|
||||
if (!commandValue.is_string())
|
||||
return {};
|
||||
|
||||
std::string command = commandValue.as_string().c_str();
|
||||
string command = commandValue.as_string().c_str();
|
||||
|
||||
return std::make_optional<Context>(
|
||||
return make_optional<Context>(
|
||||
yc,
|
||||
command,
|
||||
1,
|
||||
@@ -82,23 +84,23 @@ make_WsContext(
|
||||
clientIp);
|
||||
}
|
||||
|
||||
std::optional<Context>
|
||||
optional<Context>
|
||||
make_HttpContext(
|
||||
boost::asio::yield_context& yc,
|
||||
boost::json::object const& request,
|
||||
std::shared_ptr<BackendInterface const> const& backend,
|
||||
std::shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
std::shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
std::shared_ptr<ReportingETL const> const& etl,
|
||||
shared_ptr<BackendInterface const> const& backend,
|
||||
shared_ptr<SubscriptionManager> const& subscriptions,
|
||||
shared_ptr<ETLLoadBalancer> const& balancer,
|
||||
shared_ptr<ReportingETL const> const& etl,
|
||||
util::TagDecoratorFactory const& tagFactory,
|
||||
Backend::LedgerRange const& range,
|
||||
RPC::Counters& counters,
|
||||
std::string const& clientIp)
|
||||
string const& clientIp)
|
||||
{
|
||||
if (!request.contains("method") || !request.at("method").is_string())
|
||||
return {};
|
||||
|
||||
std::string const& command = request.at("method").as_string().c_str();
|
||||
string const& command = request.at("method").as_string().c_str();
|
||||
|
||||
if (command == "subscribe" || command == "unsubscribe")
|
||||
return {};
|
||||
@@ -114,7 +116,7 @@ make_HttpContext(
|
||||
if (!array.at(0).is_object())
|
||||
return {};
|
||||
|
||||
return std::make_optional<Context>(
|
||||
return make_optional<Context>(
|
||||
yc,
|
||||
command,
|
||||
1,
|
||||
@@ -130,107 +132,38 @@ make_HttpContext(
|
||||
clientIp);
|
||||
}
|
||||
|
||||
constexpr static WarningInfo warningInfos[]{
|
||||
{warnUNKNOWN, "Unknown warning"},
|
||||
{warnRPC_CLIO,
|
||||
"This is a clio server. clio only serves validated data. If you "
|
||||
"want to talk to rippled, include 'ledger_index':'current' in your "
|
||||
"request"},
|
||||
{warnRPC_OUTDATED, "This server may be out of date"},
|
||||
{warnRPC_RATE_LIMIT, "You are about to be rate limited"}};
|
||||
|
||||
WarningInfo const&
|
||||
get_warning_info(warning_code code)
|
||||
{
|
||||
for (WarningInfo const& info : warningInfos)
|
||||
{
|
||||
if (info.code == code)
|
||||
{
|
||||
return info;
|
||||
}
|
||||
}
|
||||
throw(std::out_of_range("Invalid warning_code"));
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
make_warning(warning_code code)
|
||||
{
|
||||
boost::json::object json;
|
||||
WarningInfo const& info(get_warning_info(code));
|
||||
json["id"] = code;
|
||||
json["message"] = static_cast<std::string>(info.message);
|
||||
return json;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
make_error(Error err)
|
||||
{
|
||||
boost::json::object json;
|
||||
ripple::RPC::ErrorInfo const& info(ripple::RPC::get_error_info(err));
|
||||
json["error"] = info.token;
|
||||
json["error_code"] = static_cast<std::uint32_t>(err);
|
||||
json["error_message"] = info.message;
|
||||
json["status"] = "error";
|
||||
json["type"] = "response";
|
||||
return json;
|
||||
}
|
||||
|
||||
boost::json::object
|
||||
make_error(Status const& status)
|
||||
{
|
||||
if (status.error == ripple::rpcUNKNOWN)
|
||||
{
|
||||
return {
|
||||
{"error", status.message},
|
||||
{"type", "response"},
|
||||
{"status", "error"}};
|
||||
}
|
||||
|
||||
boost::json::object json;
|
||||
ripple::RPC::ErrorInfo const& info(
|
||||
ripple::RPC::get_error_info(status.error));
|
||||
json["error"] =
|
||||
status.strCode.size() ? status.strCode.c_str() : info.token.c_str();
|
||||
json["error_code"] = static_cast<std::uint32_t>(status.error);
|
||||
json["error_message"] =
|
||||
status.message.size() ? status.message.c_str() : info.message.c_str();
|
||||
json["status"] = "error";
|
||||
json["type"] = "response";
|
||||
return json;
|
||||
}
|
||||
|
||||
using LimitRange = std::tuple<std::uint32_t, std::uint32_t, std::uint32_t>;
|
||||
using HandlerFunction = std::function<Result(Context const&)>;
|
||||
using LimitRange = tuple<uint32_t, uint32_t, uint32_t>;
|
||||
using HandlerFunction = function<Result(Context const&)>;
|
||||
|
||||
struct Handler
|
||||
{
|
||||
std::string method;
|
||||
std::function<Result(Context const&)> handler;
|
||||
std::optional<LimitRange> limit;
|
||||
string method;
|
||||
function<Result(Context const&)> handler;
|
||||
optional<LimitRange> limit;
|
||||
bool isClioOnly = false;
|
||||
};
|
||||
|
||||
class HandlerTable
|
||||
{
|
||||
std::unordered_map<std::string, Handler> handlerMap_;
|
||||
unordered_map<string, Handler> handlerMap_;
|
||||
|
||||
public:
|
||||
HandlerTable(std::initializer_list<Handler> handlers)
|
||||
HandlerTable(initializer_list<Handler> handlers)
|
||||
{
|
||||
for (auto const& handler : handlers)
|
||||
{
|
||||
handlerMap_[handler.method] = std::move(handler);
|
||||
handlerMap_[handler.method] = move(handler);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
contains(std::string const& method)
|
||||
contains(string const& method)
|
||||
{
|
||||
return handlerMap_.contains(method);
|
||||
}
|
||||
|
||||
std::optional<LimitRange>
|
||||
getLimitRange(std::string const& command)
|
||||
optional<LimitRange>
|
||||
getLimitRange(string const& command)
|
||||
{
|
||||
if (!handlerMap_.contains(command))
|
||||
return {};
|
||||
@@ -238,8 +171,8 @@ public:
|
||||
return handlerMap_[command].limit;
|
||||
}
|
||||
|
||||
std::optional<HandlerFunction>
|
||||
getHandler(std::string const& command)
|
||||
optional<HandlerFunction>
|
||||
getHandler(string const& command)
|
||||
{
|
||||
if (!handlerMap_.contains(command))
|
||||
return {};
|
||||
@@ -248,7 +181,7 @@ public:
|
||||
}
|
||||
|
||||
bool
|
||||
isClioOnly(std::string const& command)
|
||||
isClioOnly(string const& command)
|
||||
{
|
||||
return handlerMap_.contains(command) && handlerMap_[command].isClioOnly;
|
||||
}
|
||||
@@ -282,7 +215,7 @@ static HandlerTable handlerTable{
|
||||
{"transaction_entry", &doTransactionEntry, {}},
|
||||
{"random", &doRandom, {}}};
|
||||
|
||||
static std::unordered_set<std::string> forwardCommands{
|
||||
static unordered_set<string> forwardCommands{
|
||||
"submit",
|
||||
"submit_multisigned",
|
||||
"fee",
|
||||
@@ -294,13 +227,13 @@ static std::unordered_set<std::string> forwardCommands{
|
||||
"channel_verify"};
|
||||
|
||||
bool
|
||||
validHandler(std::string const& method)
|
||||
validHandler(string const& method)
|
||||
{
|
||||
return handlerTable.contains(method) || forwardCommands.contains(method);
|
||||
}
|
||||
|
||||
bool
|
||||
isClioOnly(std::string const& method)
|
||||
isClioOnly(string const& method)
|
||||
{
|
||||
return handlerTable.isClioOnly(method);
|
||||
}
|
||||
@@ -313,27 +246,28 @@ shouldSuppressValidatedFlag(RPC::Context const& context)
|
||||
}
|
||||
|
||||
Status
|
||||
getLimit(RPC::Context const& context, std::uint32_t& limit)
|
||||
getLimit(RPC::Context const& context, uint32_t& limit)
|
||||
{
|
||||
if (!handlerTable.getHandler(context.method))
|
||||
return Status{Error::rpcUNKNOWN_COMMAND};
|
||||
return Status{RippledError::rpcUNKNOWN_COMMAND};
|
||||
|
||||
if (!handlerTable.getLimitRange(context.method))
|
||||
return Status{Error::rpcINVALID_PARAMS, "rpcDoesNotRequireLimit"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "rpcDoesNotRequireLimit"};
|
||||
|
||||
auto [lo, def, hi] = *handlerTable.getLimitRange(context.method);
|
||||
|
||||
if (context.params.contains(JS(limit)))
|
||||
{
|
||||
std::string errMsg = "Invalid field 'limit', not unsigned integer.";
|
||||
string errMsg = "Invalid field 'limit', not unsigned integer.";
|
||||
if (!context.params.at(JS(limit)).is_int64())
|
||||
return Status{Error::rpcINVALID_PARAMS, errMsg};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, errMsg};
|
||||
|
||||
int input = context.params.at(JS(limit)).as_int64();
|
||||
if (input <= 0)
|
||||
return Status{Error::rpcINVALID_PARAMS, errMsg};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, errMsg};
|
||||
|
||||
limit = std::clamp(static_cast<std::uint32_t>(input), lo, hi);
|
||||
limit = clamp(static_cast<uint32_t>(input), lo, hi);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -378,7 +312,7 @@ buildResponse(Context const& ctx)
|
||||
ctx.counters.rpcForwarded(ctx.method);
|
||||
|
||||
if (!res)
|
||||
return Status{Error::rpcFAILED_TO_FORWARD};
|
||||
return Status{RippledError::rpcFAILED_TO_FORWARD};
|
||||
|
||||
if (res->contains("result") && res->at("result").is_object())
|
||||
return res->at("result").as_object();
|
||||
@@ -393,13 +327,13 @@ buildResponse(Context const& ctx)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< __func__ << " Database is too busy. Rejecting request";
|
||||
return Status{Error::rpcTOO_BUSY};
|
||||
return Status{RippledError::rpcTOO_BUSY};
|
||||
}
|
||||
|
||||
auto method = handlerTable.getHandler(ctx.method);
|
||||
|
||||
if (!method)
|
||||
return Status{Error::rpcUNKNOWN_COMMAND};
|
||||
return Status{RippledError::rpcUNKNOWN_COMMAND};
|
||||
|
||||
try
|
||||
{
|
||||
@@ -411,7 +345,7 @@ buildResponse(Context const& ctx)
|
||||
<< ctx.tag() << __func__ << " finish executing rpc `" << ctx.method
|
||||
<< '`';
|
||||
|
||||
if (auto object = std::get_if<boost::json::object>(&v);
|
||||
if (auto object = get_if<boost::json::object>(&v);
|
||||
object && not shouldSuppressValidatedFlag(ctx))
|
||||
{
|
||||
(*object)["validated"] = true;
|
||||
@@ -421,22 +355,22 @@ buildResponse(Context const& ctx)
|
||||
}
|
||||
catch (InvalidParamsError const& err)
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, err.what()};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, err.what()};
|
||||
}
|
||||
catch (AccountNotFoundError const& err)
|
||||
{
|
||||
return Status{Error::rpcACT_NOT_FOUND, err.what()};
|
||||
return Status{RippledError::rpcACT_NOT_FOUND, err.what()};
|
||||
}
|
||||
catch (Backend::DatabaseTimeout const& t)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __func__ << " Database timeout";
|
||||
return Status{Error::rpcTOO_BUSY};
|
||||
return Status{RippledError::rpcTOO_BUSY};
|
||||
}
|
||||
catch (std::exception const& err)
|
||||
catch (exception const& err)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< ctx.tag() << __func__ << " caught exception : " << err.what();
|
||||
return Status{Error::rpcINTERNAL};
|
||||
return Status{RippledError::rpcINTERNAL};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
103
src/rpc/RPC.h
103
src/rpc/RPC.h
@@ -1,7 +1,7 @@
|
||||
#ifndef REPORTING_RPC_H_INCLUDED
|
||||
#define REPORTING_RPC_H_INCLUDED
|
||||
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <rpc/Errors.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/json.hpp>
|
||||
@@ -65,7 +65,6 @@ struct Context : public util::Taggable
|
||||
Counters& counters_,
|
||||
std::string const& clientIp_);
|
||||
};
|
||||
using Error = ripple::error_code_i;
|
||||
|
||||
struct AccountCursor
|
||||
{
|
||||
@@ -85,108 +84,8 @@ struct AccountCursor
|
||||
}
|
||||
};
|
||||
|
||||
struct Status
|
||||
{
|
||||
Error error = Error::rpcSUCCESS;
|
||||
std::string strCode = "";
|
||||
std::string message = "";
|
||||
|
||||
Status(){};
|
||||
|
||||
Status(Error error_) : error(error_){};
|
||||
|
||||
// HACK. Some rippled handlers explicitly specify errors.
|
||||
// This means that we have to be able to duplicate this
|
||||
// functionality.
|
||||
Status(std::string const& message_)
|
||||
: error(ripple::rpcUNKNOWN), message(message_)
|
||||
{
|
||||
}
|
||||
|
||||
Status(Error error_, std::string message_)
|
||||
: error(error_), message(message_)
|
||||
{
|
||||
}
|
||||
|
||||
Status(Error error_, std::string strCode_, std::string message_)
|
||||
: error(error_), strCode(strCode_), message(message_)
|
||||
{
|
||||
}
|
||||
|
||||
/** Returns true if the Status is *not* OK. */
|
||||
operator bool() const
|
||||
{
|
||||
return error != Error::rpcSUCCESS;
|
||||
}
|
||||
};
|
||||
|
||||
static Status OK;
|
||||
|
||||
using Result = std::variant<Status, boost::json::object>;
|
||||
|
||||
class InvalidParamsError : public std::exception
|
||||
{
|
||||
std::string msg;
|
||||
|
||||
public:
|
||||
InvalidParamsError(std::string const& msg) : msg(msg)
|
||||
{
|
||||
}
|
||||
|
||||
const char*
|
||||
what() const throw() override
|
||||
{
|
||||
return msg.c_str();
|
||||
}
|
||||
};
|
||||
class AccountNotFoundError : public std::exception
|
||||
{
|
||||
std::string account;
|
||||
|
||||
public:
|
||||
AccountNotFoundError(std::string const& acct) : account(acct)
|
||||
{
|
||||
}
|
||||
const char*
|
||||
what() const throw() override
|
||||
{
|
||||
return account.c_str();
|
||||
}
|
||||
};
|
||||
|
||||
enum warning_code {
|
||||
warnUNKNOWN = -1,
|
||||
warnRPC_CLIO = 2001,
|
||||
warnRPC_OUTDATED = 2002,
|
||||
warnRPC_RATE_LIMIT = 2003
|
||||
};
|
||||
|
||||
struct WarningInfo
|
||||
{
|
||||
constexpr WarningInfo() : code(warnUNKNOWN), message("unknown warning")
|
||||
{
|
||||
}
|
||||
|
||||
constexpr WarningInfo(warning_code code_, char const* message_)
|
||||
: code(code_), message(message_)
|
||||
{
|
||||
}
|
||||
warning_code code;
|
||||
std::string_view const message;
|
||||
};
|
||||
|
||||
WarningInfo const&
|
||||
get_warning_info(warning_code code);
|
||||
|
||||
boost::json::object
|
||||
make_warning(warning_code code);
|
||||
|
||||
boost::json::object
|
||||
make_error(Status const& status);
|
||||
|
||||
boost::json::object
|
||||
make_error(Error err);
|
||||
|
||||
std::optional<Context>
|
||||
make_WsContext(
|
||||
boost::asio::yield_context& yc,
|
||||
|
||||
@@ -187,10 +187,10 @@ getHexMarker(boost::json::object const& request, ripple::uint256& marker)
|
||||
if (request.contains(JS(marker)))
|
||||
{
|
||||
if (!request.at(JS(marker)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "markerNotString"};
|
||||
|
||||
if (!marker.parseHex(request.at(JS(marker)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedMarker"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedMarker"};
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -207,14 +207,14 @@ getAccount(
|
||||
{
|
||||
if (required)
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, field.to_string() + "Missing"};
|
||||
RippledError::rpcINVALID_PARAMS, field.to_string() + "Missing"};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!request.at(field).is_string())
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, field.to_string() + "NotString"};
|
||||
RippledError::rpcINVALID_PARAMS, field.to_string() + "NotString"};
|
||||
|
||||
if (auto a = accountFromStringStrict(request.at(field).as_string().c_str());
|
||||
a)
|
||||
@@ -223,7 +223,8 @@ getAccount(
|
||||
return {};
|
||||
}
|
||||
|
||||
return Status{Error::rpcACT_MALFORMED, field.to_string() + "Malformed"};
|
||||
return Status{
|
||||
RippledError::rpcACT_MALFORMED, field.to_string() + "Malformed"};
|
||||
}
|
||||
|
||||
Status
|
||||
@@ -240,7 +241,7 @@ getOptionalAccount(
|
||||
|
||||
if (!request.at(field).is_string())
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, field.to_string() + "NotString"};
|
||||
RippledError::rpcINVALID_PARAMS, field.to_string() + "NotString"};
|
||||
|
||||
if (auto a = accountFromStringStrict(request.at(field).as_string().c_str());
|
||||
a)
|
||||
@@ -249,7 +250,8 @@ getOptionalAccount(
|
||||
return {};
|
||||
}
|
||||
|
||||
return Status{Error::rpcINVALID_PARAMS, field.to_string() + "Malformed"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, field.to_string() + "Malformed"};
|
||||
}
|
||||
|
||||
Status
|
||||
@@ -286,13 +288,13 @@ Status
|
||||
getChannelId(boost::json::object const& request, ripple::uint256& channelId)
|
||||
{
|
||||
if (!request.contains(JS(channel_id)))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingChannelID"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "missingChannelID"};
|
||||
|
||||
if (!request.at(JS(channel_id)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "channelIDNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "channelIDNotString"};
|
||||
|
||||
if (!channelId.parseHex(request.at(JS(channel_id)).as_string().c_str()))
|
||||
return Status{Error::rpcCHANNEL_MALFORMED, "malformedChannelID"};
|
||||
return Status{RippledError::rpcCHANNEL_MALFORMED, "malformedChannelID"};
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -554,16 +556,18 @@ ledgerInfoFromRequest(Context const& ctx)
|
||||
if (!hashValue.is_null())
|
||||
{
|
||||
if (!hashValue.is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "ledgerHashNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "ledgerHashNotString"};
|
||||
|
||||
ripple::uint256 ledgerHash;
|
||||
if (!ledgerHash.parseHex(hashValue.as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "ledgerHashMalformed"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "ledgerHashMalformed"};
|
||||
|
||||
auto lgrInfo = ctx.backend->fetchLedgerByHash(ledgerHash, ctx.yield);
|
||||
|
||||
if (!lgrInfo || lgrInfo->seq > ctx.range.maxSequence)
|
||||
return Status{Error::rpcLGR_NOT_FOUND, "ledgerNotFound"};
|
||||
return Status{RippledError::rpcLGR_NOT_FOUND, "ledgerNotFound"};
|
||||
|
||||
return *lgrInfo;
|
||||
}
|
||||
@@ -592,13 +596,13 @@ ledgerInfoFromRequest(Context const& ctx)
|
||||
}
|
||||
|
||||
if (!ledgerSequence)
|
||||
return Status{Error::rpcINVALID_PARAMS, "ledgerIndexMalformed"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "ledgerIndexMalformed"};
|
||||
|
||||
auto lgrInfo =
|
||||
ctx.backend->fetchLedgerBySequence(*ledgerSequence, ctx.yield);
|
||||
|
||||
if (!lgrInfo || lgrInfo->seq > ctx.range.maxSequence)
|
||||
return Status{Error::rpcLGR_NOT_FOUND, "ledgerNotFound"};
|
||||
return Status{RippledError::rpcLGR_NOT_FOUND, "ledgerNotFound"};
|
||||
|
||||
return *lgrInfo;
|
||||
}
|
||||
@@ -651,7 +655,7 @@ traverseOwnedNodes(
|
||||
{
|
||||
if (!backend.fetchLedgerObject(
|
||||
ripple::keylet::account(accountID).key, sequence, yield))
|
||||
return Status{Error::rpcACT_NOT_FOUND};
|
||||
return Status{RippledError::rpcACT_NOT_FOUND};
|
||||
|
||||
auto parsedCursor =
|
||||
parseAccountCursor(backend, sequence, jsonCursor, accountID, yield);
|
||||
@@ -891,12 +895,12 @@ keypairFromRequst(boost::json::object const& request)
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
return Status{Error::rpcINVALID_PARAMS, "missing field secret"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "missing field secret"};
|
||||
|
||||
if (count > 1)
|
||||
{
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS,
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"Exactly one of the following must be specified: "
|
||||
" passphrase, secret, seed, or seed_hex"};
|
||||
}
|
||||
@@ -907,17 +911,18 @@ keypairFromRequst(boost::json::object const& request)
|
||||
if (has_key_type)
|
||||
{
|
||||
if (!request.at("key_type").is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "keyTypeNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "keyTypeNotString"};
|
||||
|
||||
std::string key_type = request.at("key_type").as_string().c_str();
|
||||
keyType = ripple::keyTypeFromString(key_type);
|
||||
|
||||
if (!keyType)
|
||||
return Status{Error::rpcINVALID_PARAMS, "invalidFieldKeyType"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "invalidFieldKeyType"};
|
||||
|
||||
if (secretType == "secret")
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS,
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"The secret field is not allowed if key_type is used."};
|
||||
}
|
||||
|
||||
@@ -935,7 +940,7 @@ keypairFromRequst(boost::json::object const& request)
|
||||
if (keyType.value_or(ripple::KeyType::ed25519) !=
|
||||
ripple::KeyType::ed25519)
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS,
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"Specified seed is for an Ed25519 wallet."};
|
||||
|
||||
keyType = ripple::KeyType::ed25519;
|
||||
@@ -951,7 +956,8 @@ keypairFromRequst(boost::json::object const& request)
|
||||
{
|
||||
if (!request.at(secretType).is_string())
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, "secret value must be string"};
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"secret value must be string"};
|
||||
|
||||
std::string key = request.at(secretType).as_string().c_str();
|
||||
|
||||
@@ -970,7 +976,7 @@ keypairFromRequst(boost::json::object const& request)
|
||||
{
|
||||
if (!request.at("secret").is_string())
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS,
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"field secret should be a string"};
|
||||
|
||||
std::string secret = request.at("secret").as_string().c_str();
|
||||
@@ -980,12 +986,14 @@ keypairFromRequst(boost::json::object const& request)
|
||||
|
||||
if (!seed)
|
||||
return Status{
|
||||
Error::rpcBAD_SEED, "Bad Seed: invalid field message secretType"};
|
||||
RippledError::rpcBAD_SEED,
|
||||
"Bad Seed: invalid field message secretType"};
|
||||
|
||||
if (keyType != ripple::KeyType::secp256k1 &&
|
||||
keyType != ripple::KeyType::ed25519)
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, "keypairForSignature: invalid key type"};
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"keypairForSignature: invalid key type"};
|
||||
|
||||
return generateKeyPair(*keyType, *seed);
|
||||
}
|
||||
@@ -1343,54 +1351,63 @@ std::variant<Status, ripple::Book>
|
||||
parseBook(boost::json::object const& request)
|
||||
{
|
||||
if (!request.contains("taker_pays"))
|
||||
return Status{Error::rpcBAD_MARKET, "missingTakerPays"};
|
||||
return Status{RippledError::rpcBAD_MARKET, "missingTakerPays"};
|
||||
|
||||
if (!request.contains("taker_gets"))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingTakerGets"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "missingTakerGets"};
|
||||
|
||||
if (!request.at("taker_pays").is_object())
|
||||
return Status{Error::rpcINVALID_PARAMS, "takerPaysNotObject"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "takerPaysNotObject"};
|
||||
|
||||
if (!request.at("taker_gets").is_object())
|
||||
return Status{Error::rpcINVALID_PARAMS, "takerGetsNotObject"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "takerGetsNotObject"};
|
||||
|
||||
auto taker_pays = request.at("taker_pays").as_object();
|
||||
if (!taker_pays.contains("currency"))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingTakerPaysCurrency"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "missingTakerPaysCurrency"};
|
||||
|
||||
if (!taker_pays.at("currency").is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "takerPaysCurrencyNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "takerPaysCurrencyNotString"};
|
||||
|
||||
auto taker_gets = request.at("taker_gets").as_object();
|
||||
if (!taker_gets.contains("currency"))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingTakerGetsCurrency"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "missingTakerGetsCurrency"};
|
||||
|
||||
if (!taker_gets.at("currency").is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "takerGetsCurrencyNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "takerGetsCurrencyNotString"};
|
||||
|
||||
ripple::Currency pay_currency;
|
||||
if (!ripple::to_currency(
|
||||
pay_currency, taker_pays.at("currency").as_string().c_str()))
|
||||
return Status{Error::rpcSRC_CUR_MALFORMED, "badTakerPaysCurrency"};
|
||||
return Status{
|
||||
RippledError::rpcSRC_CUR_MALFORMED, "badTakerPaysCurrency"};
|
||||
|
||||
ripple::Currency get_currency;
|
||||
if (!ripple::to_currency(
|
||||
get_currency, taker_gets["currency"].as_string().c_str()))
|
||||
return Status{Error::rpcDST_AMT_MALFORMED, "badTakerGetsCurrency"};
|
||||
return Status{
|
||||
RippledError::rpcDST_AMT_MALFORMED, "badTakerGetsCurrency"};
|
||||
|
||||
ripple::AccountID pay_issuer;
|
||||
if (taker_pays.contains("issuer"))
|
||||
{
|
||||
if (!taker_pays.at("issuer").is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "takerPaysIssuerNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "takerPaysIssuerNotString"};
|
||||
|
||||
if (!ripple::to_issuer(
|
||||
pay_issuer, taker_pays.at("issuer").as_string().c_str()))
|
||||
return Status{Error::rpcSRC_ISR_MALFORMED, "badTakerPaysIssuer"};
|
||||
return Status{
|
||||
RippledError::rpcSRC_ISR_MALFORMED, "badTakerPaysIssuer"};
|
||||
|
||||
if (pay_issuer == ripple::noAccount())
|
||||
return Status{
|
||||
Error::rpcSRC_ISR_MALFORMED, "badTakerPaysIssuerAccountOne"};
|
||||
RippledError::rpcSRC_ISR_MALFORMED,
|
||||
"badTakerPaysIssuerAccountOne"};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1399,18 +1416,19 @@ parseBook(boost::json::object const& request)
|
||||
|
||||
if (isXRP(pay_currency) && !isXRP(pay_issuer))
|
||||
return Status{
|
||||
Error::rpcSRC_ISR_MALFORMED,
|
||||
RippledError::rpcSRC_ISR_MALFORMED,
|
||||
"Unneeded field 'taker_pays.issuer' for XRP currency "
|
||||
"specification."};
|
||||
|
||||
if (!isXRP(pay_currency) && isXRP(pay_issuer))
|
||||
return Status{
|
||||
Error::rpcSRC_ISR_MALFORMED,
|
||||
RippledError::rpcSRC_ISR_MALFORMED,
|
||||
"Invalid field 'taker_pays.issuer', expected non-XRP "
|
||||
"issuer."};
|
||||
|
||||
if ((!isXRP(pay_currency)) && (!taker_pays.contains("issuer")))
|
||||
return Status{Error::rpcSRC_ISR_MALFORMED, "Missing non-XRP issuer."};
|
||||
return Status{
|
||||
RippledError::rpcSRC_ISR_MALFORMED, "Missing non-XRP issuer."};
|
||||
|
||||
ripple::AccountID get_issuer;
|
||||
|
||||
@@ -1418,17 +1436,18 @@ parseBook(boost::json::object const& request)
|
||||
{
|
||||
if (!taker_gets["issuer"].is_string())
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, "taker_gets.issuer should be string"};
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"taker_gets.issuer should be string"};
|
||||
|
||||
if (!ripple::to_issuer(
|
||||
get_issuer, taker_gets.at("issuer").as_string().c_str()))
|
||||
return Status{
|
||||
Error::rpcDST_ISR_MALFORMED,
|
||||
RippledError::rpcDST_ISR_MALFORMED,
|
||||
"Invalid field 'taker_gets.issuer', bad issuer."};
|
||||
|
||||
if (get_issuer == ripple::noAccount())
|
||||
return Status{
|
||||
Error::rpcDST_ISR_MALFORMED,
|
||||
RippledError::rpcDST_ISR_MALFORMED,
|
||||
"Invalid field 'taker_gets.issuer', bad issuer account "
|
||||
"one."};
|
||||
}
|
||||
@@ -1439,17 +1458,17 @@ parseBook(boost::json::object const& request)
|
||||
|
||||
if (ripple::isXRP(get_currency) && !ripple::isXRP(get_issuer))
|
||||
return Status{
|
||||
Error::rpcDST_ISR_MALFORMED,
|
||||
RippledError::rpcDST_ISR_MALFORMED,
|
||||
"Unneeded field 'taker_gets.issuer' for XRP currency "
|
||||
"specification."};
|
||||
|
||||
if (!ripple::isXRP(get_currency) && ripple::isXRP(get_issuer))
|
||||
return Status{
|
||||
Error::rpcDST_ISR_MALFORMED,
|
||||
RippledError::rpcDST_ISR_MALFORMED,
|
||||
"Invalid field 'taker_gets.issuer', expected non-XRP issuer."};
|
||||
|
||||
if (pay_currency == get_currency && pay_issuer == get_issuer)
|
||||
return Status{Error::rpcBAD_MARKET, "badMarket"};
|
||||
return Status{RippledError::rpcBAD_MARKET, "badMarket"};
|
||||
|
||||
return ripple::Book{{pay_currency, pay_issuer}, {get_currency, get_issuer}};
|
||||
}
|
||||
@@ -1459,12 +1478,12 @@ parseTaker(boost::json::value const& taker)
|
||||
{
|
||||
std::optional<ripple::AccountID> takerID = {};
|
||||
if (!taker.is_string())
|
||||
return {Status{Error::rpcINVALID_PARAMS, "takerNotString"}};
|
||||
return {Status{RippledError::rpcINVALID_PARAMS, "takerNotString"}};
|
||||
|
||||
takerID = accountFromStringStrict(taker.as_string().c_str());
|
||||
|
||||
if (!takerID)
|
||||
return Status{Error::rpcBAD_ISSUER, "invalidTakerAccount"};
|
||||
return Status{RippledError::rpcBAD_ISSUER, "invalidTakerAccount"};
|
||||
return *takerID;
|
||||
}
|
||||
bool
|
||||
@@ -1486,14 +1505,14 @@ std::variant<ripple::uint256, Status>
|
||||
getNFTID(boost::json::object const& request)
|
||||
{
|
||||
if (!request.contains(JS(nft_id)))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingTokenID"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "missingTokenID"};
|
||||
|
||||
if (!request.at(JS(nft_id)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "tokenIDNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "tokenIDNotString"};
|
||||
|
||||
ripple::uint256 tokenid;
|
||||
if (!tokenid.parseHex(request.at(JS(nft_id)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedTokenID"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedTokenID"};
|
||||
|
||||
return tokenid;
|
||||
}
|
||||
@@ -1521,7 +1540,7 @@ traverseTransactions(
|
||||
if (request.contains(JS(marker)))
|
||||
{
|
||||
if (!request.at(JS(marker)).is_object())
|
||||
return Status{Error::rpcINVALID_PARAMS, "invalidMarker"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "invalidMarker"};
|
||||
auto const& obj = request.at(JS(marker)).as_object();
|
||||
|
||||
std::optional<std::uint32_t> transactionIndex = {};
|
||||
@@ -1529,7 +1548,7 @@ traverseTransactions(
|
||||
{
|
||||
if (!obj.at(JS(seq)).is_int64())
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, "transactionIndexNotInt"};
|
||||
RippledError::rpcINVALID_PARAMS, "transactionIndexNotInt"};
|
||||
|
||||
transactionIndex =
|
||||
boost::json::value_to<std::uint32_t>(obj.at(JS(seq)));
|
||||
@@ -1539,14 +1558,16 @@ traverseTransactions(
|
||||
if (obj.contains(JS(ledger)))
|
||||
{
|
||||
if (!obj.at(JS(ledger)).is_int64())
|
||||
return Status{Error::rpcINVALID_PARAMS, "ledgerIndexNotInt"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "ledgerIndexNotInt"};
|
||||
|
||||
ledgerIndex =
|
||||
boost::json::value_to<std::uint32_t>(obj.at(JS(ledger)));
|
||||
}
|
||||
|
||||
if (!transactionIndex || !ledgerIndex)
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingLedgerOrSeq"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "missingLedgerOrSeq"};
|
||||
|
||||
cursor = {*ledgerIndex, *transactionIndex};
|
||||
}
|
||||
@@ -1557,14 +1578,16 @@ traverseTransactions(
|
||||
auto& min = request.at(JS(ledger_index_min));
|
||||
|
||||
if (!min.is_int64())
|
||||
return Status{Error::rpcINVALID_PARAMS, "ledgerSeqMinNotNumber"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "ledgerSeqMinNotNumber"};
|
||||
|
||||
if (min.as_int64() != -1)
|
||||
{
|
||||
if (context.range.maxSequence < min.as_int64() ||
|
||||
context.range.minSequence > min.as_int64())
|
||||
return Status{
|
||||
Error::rpcLGR_IDX_MALFORMED, "ledgerSeqMinOutOfRange"};
|
||||
RippledError::rpcLGR_IDX_MALFORMED,
|
||||
"ledgerSeqMinOutOfRange"};
|
||||
else
|
||||
minIndex = boost::json::value_to<std::uint32_t>(min);
|
||||
}
|
||||
@@ -1579,20 +1602,22 @@ traverseTransactions(
|
||||
auto& max = request.at(JS(ledger_index_max));
|
||||
|
||||
if (!max.is_int64())
|
||||
return Status{Error::rpcINVALID_PARAMS, "ledgerSeqMaxNotNumber"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "ledgerSeqMaxNotNumber"};
|
||||
|
||||
if (max.as_int64() != -1)
|
||||
{
|
||||
if (context.range.maxSequence < max.as_int64() ||
|
||||
context.range.minSequence > max.as_int64())
|
||||
return Status{
|
||||
Error::rpcLGR_IDX_MALFORMED, "ledgerSeqMaxOutOfRange"};
|
||||
RippledError::rpcLGR_IDX_MALFORMED,
|
||||
"ledgerSeqMaxOutOfRange"};
|
||||
else
|
||||
maxIndex = boost::json::value_to<std::uint32_t>(max);
|
||||
}
|
||||
|
||||
if (minIndex > maxIndex)
|
||||
return Status{Error::rpcINVALID_PARAMS, "invalidIndex"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "invalidIndex"};
|
||||
|
||||
if (!forward && !cursor)
|
||||
cursor = {maxIndex, INT32_MAX};
|
||||
@@ -1603,7 +1628,8 @@ traverseTransactions(
|
||||
if (request.contains(JS(ledger_index_max)) ||
|
||||
request.contains(JS(ledger_index_min)))
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, "containsLedgerSpecifierAndRange"};
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"containsLedgerSpecifierAndRange"};
|
||||
|
||||
auto v = ledgerInfoFromRequest(context);
|
||||
if (auto status = std::get_if<Status>(&v); status)
|
||||
|
||||
@@ -62,7 +62,7 @@ doAccountChannels(Context const& context)
|
||||
ripple::keylet::account(accountID).key, lgrInfo.seq, context.yield);
|
||||
|
||||
if (!rawAcct)
|
||||
return Status{Error::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
return Status{RippledError::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
|
||||
ripple::AccountID destAccount;
|
||||
if (auto const status =
|
||||
@@ -78,7 +78,7 @@ doAccountChannels(Context const& context)
|
||||
if (request.contains(JS(marker)))
|
||||
{
|
||||
if (!request.at(JS(marker)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "markerNotString"};
|
||||
|
||||
marker = request.at(JS(marker)).as_string().c_str();
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ doAccountCurrencies(Context const& context)
|
||||
ripple::keylet::account(accountID).key, lgrInfo.seq, context.yield);
|
||||
|
||||
if (!rawAcct)
|
||||
return Status{Error::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
return Status{RippledError::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
|
||||
std::set<std::string> send, receive;
|
||||
auto const addToResponse = [&](ripple::SLE&& sle) {
|
||||
|
||||
@@ -34,7 +34,7 @@ doAccountInfo(Context const& context)
|
||||
else if (request.contains(JS(ident)))
|
||||
strIdent = request.at(JS(ident)).as_string().c_str();
|
||||
else
|
||||
return Status{Error::rpcACT_MALFORMED};
|
||||
return Status{RippledError::rpcACT_MALFORMED};
|
||||
|
||||
// We only need to fetch the ledger header because the ledger hash is
|
||||
// supposed to be included in the response. The ledger sequence is specified
|
||||
@@ -48,7 +48,7 @@ doAccountInfo(Context const& context)
|
||||
// Get info on account.
|
||||
auto accountID = accountFromStringStrict(strIdent);
|
||||
if (!accountID)
|
||||
return Status{Error::rpcACT_MALFORMED};
|
||||
return Status{RippledError::rpcACT_MALFORMED};
|
||||
|
||||
assert(accountID.has_value());
|
||||
|
||||
@@ -59,14 +59,14 @@ doAccountInfo(Context const& context)
|
||||
|
||||
if (!dbResponse)
|
||||
{
|
||||
return Status{Error::rpcACT_NOT_FOUND};
|
||||
return Status{RippledError::rpcACT_NOT_FOUND};
|
||||
}
|
||||
|
||||
ripple::STLedgerEntry sle{
|
||||
ripple::SerialIter{dbResponse->data(), dbResponse->size()}, key.key};
|
||||
|
||||
if (!key.check(sle))
|
||||
return Status{Error::rpcDB_DESERIALIZATION};
|
||||
return Status{RippledError::rpcDB_DESERIALIZATION};
|
||||
|
||||
// if (!binary)
|
||||
// response[JS(account_data)] = getJson(sle);
|
||||
@@ -97,7 +97,7 @@ doAccountInfo(Context const& context)
|
||||
ripple::SerialIter{signers->data(), signers->size()},
|
||||
signersKey.key};
|
||||
if (!signersKey.check(sleSigners))
|
||||
return Status{Error::rpcDB_DESERIALIZATION};
|
||||
return Status{RippledError::rpcDB_DESERIALIZATION};
|
||||
|
||||
signerList.push_back(toJson(sleSigners));
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ doAccountLines(Context const& context)
|
||||
ripple::keylet::account(accountID).key, lgrInfo.seq, context.yield);
|
||||
|
||||
if (!rawAcct)
|
||||
return Status{Error::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
return Status{RippledError::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
|
||||
std::optional<ripple::AccountID> peerAccount;
|
||||
if (auto const status = getOptionalAccount(request, peerAccount, JS(peer));
|
||||
@@ -122,7 +122,7 @@ doAccountLines(Context const& context)
|
||||
if (request.contains(JS(marker)))
|
||||
{
|
||||
if (not request.at(JS(marker)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "markerNotString"};
|
||||
|
||||
marker = request.at(JS(marker)).as_string().c_str();
|
||||
}
|
||||
@@ -131,7 +131,8 @@ doAccountLines(Context const& context)
|
||||
if (request.contains(JS(ignore_default)))
|
||||
{
|
||||
if (not request.at(JS(ignore_default)).is_bool())
|
||||
return Status{Error::rpcINVALID_PARAMS, "ignoreDefaultNotBool"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "ignoreDefaultNotBool"};
|
||||
|
||||
ignoreDefault = request.at(JS(ignore_default)).as_bool();
|
||||
}
|
||||
|
||||
@@ -45,13 +45,13 @@ doAccountNFTs(Context const& context)
|
||||
return status;
|
||||
|
||||
if (!accountID)
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedAccount"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedAccount"};
|
||||
|
||||
auto rawAcct = context.backend->fetchLedgerObject(
|
||||
ripple::keylet::account(accountID).key, lgrInfo.seq, context.yield);
|
||||
|
||||
if (!rawAcct)
|
||||
return Status{Error::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
return Status{RippledError::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
|
||||
std::uint32_t limit;
|
||||
if (auto const status = getLimit(context, limit); status)
|
||||
@@ -157,7 +157,7 @@ doAccountObjects(Context const& context)
|
||||
if (request.contains("marker"))
|
||||
{
|
||||
if (!request.at("marker").is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "markerNotString"};
|
||||
|
||||
marker = request.at("marker").as_string().c_str();
|
||||
}
|
||||
@@ -166,11 +166,11 @@ doAccountObjects(Context const& context)
|
||||
if (request.contains(JS(type)))
|
||||
{
|
||||
if (!request.at(JS(type)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "typeNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "typeNotString"};
|
||||
|
||||
std::string typeAsString = request.at(JS(type)).as_string().c_str();
|
||||
if (types.find(typeAsString) == types.end())
|
||||
return Status{Error::rpcINVALID_PARAMS, "typeInvalid"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "typeInvalid"};
|
||||
|
||||
objectType = types[typeAsString];
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ doAccountOffers(Context const& context)
|
||||
ripple::keylet::account(accountID).key, lgrInfo.seq, context.yield);
|
||||
|
||||
if (!rawAcct)
|
||||
return Status{Error::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
return Status{RippledError::rpcACT_NOT_FOUND, "accountNotFound"};
|
||||
|
||||
std::uint32_t limit;
|
||||
if (auto const status = getLimit(context, limit); status)
|
||||
@@ -94,7 +94,7 @@ doAccountOffers(Context const& context)
|
||||
if (request.contains(JS(marker)))
|
||||
{
|
||||
if (!request.at(JS(marker)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "markerNotString"};
|
||||
|
||||
marker = request.at(JS(marker)).as_string().c_str();
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ doBookOffers(Context const& context)
|
||||
if (request.contains("book"))
|
||||
{
|
||||
if (!request.at("book").is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "bookNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "bookNotString"};
|
||||
|
||||
if (!bookBase.parseHex(request.at("book").as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "invalidBook"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "invalidBook"};
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -28,13 +28,14 @@ doChannelAuthorize(Context const& context)
|
||||
boost::json::object response = {};
|
||||
|
||||
if (!request.contains(JS(amount)))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingAmount"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "missingAmount"};
|
||||
|
||||
if (!request.at(JS(amount)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "amountNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "amountNotString"};
|
||||
|
||||
if (!request.contains(JS(key_type)) && !request.contains(JS(secret)))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingKeyTypeOrSecret"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "missingKeyTypeOrSecret"};
|
||||
|
||||
auto v = keypairFromRequst(request);
|
||||
if (auto status = std::get_if<Status>(&v))
|
||||
@@ -51,7 +52,8 @@ doChannelAuthorize(Context const& context)
|
||||
ripple::to_uint64(request.at(JS(amount)).as_string().c_str());
|
||||
|
||||
if (!optDrops)
|
||||
return Status{Error::rpcCHANNEL_AMT_MALFORMED, "couldNotParseAmount"};
|
||||
return Status{
|
||||
RippledError::rpcCHANNEL_AMT_MALFORMED, "couldNotParseAmount"};
|
||||
|
||||
std::uint64_t drops = *optDrops;
|
||||
|
||||
@@ -66,7 +68,7 @@ doChannelAuthorize(Context const& context)
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
return Status{Error::rpcINTERNAL};
|
||||
return Status{RippledError::rpcINTERNAL};
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -17,22 +17,22 @@ doChannelVerify(Context const& context)
|
||||
boost::json::object response = {};
|
||||
|
||||
if (!request.contains(JS(amount)))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingAmount"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "missingAmount"};
|
||||
|
||||
if (!request.at(JS(amount)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "amountNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "amountNotString"};
|
||||
|
||||
if (!request.contains(JS(signature)))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingSignature"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "missingSignature"};
|
||||
|
||||
if (!request.at(JS(signature)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "signatureNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "signatureNotString"};
|
||||
|
||||
if (!request.contains(JS(public_key)))
|
||||
return Status{Error::rpcINVALID_PARAMS, "missingPublicKey"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "missingPublicKey"};
|
||||
|
||||
if (!request.at(JS(public_key)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "publicKeyNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "publicKeyNotString"};
|
||||
|
||||
std::optional<ripple::PublicKey> pk;
|
||||
{
|
||||
@@ -45,12 +45,14 @@ doChannelVerify(Context const& context)
|
||||
{
|
||||
auto pkHex = ripple::strUnHex(strPk);
|
||||
if (!pkHex)
|
||||
return Status{Error::rpcPUBLIC_MALFORMED, "malformedPublicKey"};
|
||||
return Status{
|
||||
RippledError::rpcPUBLIC_MALFORMED, "malformedPublicKey"};
|
||||
|
||||
auto const pkType =
|
||||
ripple::publicKeyType(ripple::makeSlice(*pkHex));
|
||||
if (!pkType)
|
||||
return Status{Error::rpcPUBLIC_MALFORMED, "invalidKeyType"};
|
||||
return Status{
|
||||
RippledError::rpcPUBLIC_MALFORMED, "invalidKeyType"};
|
||||
|
||||
pk.emplace(ripple::makeSlice(*pkHex));
|
||||
}
|
||||
@@ -64,14 +66,15 @@ doChannelVerify(Context const& context)
|
||||
ripple::to_uint64(request.at(JS(amount)).as_string().c_str());
|
||||
|
||||
if (!optDrops)
|
||||
return Status{Error::rpcCHANNEL_AMT_MALFORMED, "couldNotParseAmount"};
|
||||
return Status{
|
||||
RippledError::rpcCHANNEL_AMT_MALFORMED, "couldNotParseAmount"};
|
||||
|
||||
std::uint64_t drops = *optDrops;
|
||||
|
||||
auto sig = ripple::strUnHex(request.at(JS(signature)).as_string().c_str());
|
||||
|
||||
if (!sig || !sig->size())
|
||||
return Status{Error::rpcINVALID_PARAMS, "invalidSignature"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "invalidSignature"};
|
||||
|
||||
ripple::Serializer msg;
|
||||
ripple::serializePayChanAuthorization(
|
||||
|
||||
@@ -187,7 +187,7 @@ doGatewayBalances(Context const& context)
|
||||
};
|
||||
if (not std::all_of(
|
||||
hotWallets.begin(), hotWallets.end(), containsHotWallet))
|
||||
return Status{Error::rpcINVALID_PARAMS, "invalidHotWallet"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "invalidHotWallet"};
|
||||
|
||||
if (auto balances = toJson(hotBalances); balances.size())
|
||||
response[JS(balances)] = balances;
|
||||
|
||||
@@ -13,7 +13,7 @@ doLedger(Context const& context)
|
||||
if (params.contains(JS(binary)))
|
||||
{
|
||||
if (!params.at(JS(binary)).is_bool())
|
||||
return Status{Error::rpcINVALID_PARAMS, "binaryFlagNotBool"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "binaryFlagNotBool"};
|
||||
|
||||
binary = params.at(JS(binary)).as_bool();
|
||||
}
|
||||
@@ -22,7 +22,8 @@ doLedger(Context const& context)
|
||||
if (params.contains(JS(transactions)))
|
||||
{
|
||||
if (!params.at(JS(transactions)).is_bool())
|
||||
return Status{Error::rpcINVALID_PARAMS, "transactionsFlagNotBool"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "transactionsFlagNotBool"};
|
||||
|
||||
transactions = params.at(JS(transactions)).as_bool();
|
||||
}
|
||||
@@ -31,7 +32,7 @@ doLedger(Context const& context)
|
||||
if (params.contains(JS(expand)))
|
||||
{
|
||||
if (!params.at(JS(expand)).is_bool())
|
||||
return Status{Error::rpcINVALID_PARAMS, "expandFlagNotBool"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "expandFlagNotBool"};
|
||||
|
||||
expand = params.at(JS(expand)).as_bool();
|
||||
}
|
||||
@@ -40,7 +41,7 @@ doLedger(Context const& context)
|
||||
if (params.contains("diff"))
|
||||
{
|
||||
if (!params.at("diff").is_bool())
|
||||
return Status{Error::rpcINVALID_PARAMS, "diffFlagNotBool"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "diffFlagNotBool"};
|
||||
|
||||
diff = params.at("diff").as_bool();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ doLedgerData(Context const& context)
|
||||
if (request.contains("out_of_order"))
|
||||
{
|
||||
if (!request.at("out_of_order").is_bool())
|
||||
return Status{Error::rpcINVALID_PARAMS, "binaryFlagNotBool"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "binaryFlagNotBool"};
|
||||
outOfOrder = request.at("out_of_order").as_bool();
|
||||
}
|
||||
|
||||
@@ -55,11 +55,13 @@ doLedgerData(Context const& context)
|
||||
{
|
||||
if (!request.at(JS(marker)).is_int64())
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, "markerNotStringOrInt"};
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"markerNotStringOrInt"};
|
||||
diffMarker = value_to<uint32_t>(request.at(JS(marker)));
|
||||
}
|
||||
else
|
||||
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "markerNotString"};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -67,7 +69,8 @@ doLedgerData(Context const& context)
|
||||
|
||||
marker = ripple::uint256{};
|
||||
if (!marker->parseHex(request.at(JS(marker)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "markerMalformed"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "markerMalformed"};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +118,8 @@ doLedgerData(Context const& context)
|
||||
if (!outOfOrder &&
|
||||
!context.backend->fetchLedgerObject(
|
||||
*marker, lgrInfo.seq, context.yield))
|
||||
return Status{Error::rpcINVALID_PARAMS, "markerDoesNotExist"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "markerDoesNotExist"};
|
||||
}
|
||||
|
||||
response[JS(ledger_hash)] = ripple::strHex(lgrInfo.hash);
|
||||
|
||||
@@ -32,31 +32,32 @@ doLedgerEntry(Context const& context)
|
||||
if (request.contains(JS(index)))
|
||||
{
|
||||
if (!request.at(JS(index)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "indexNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "indexNotString"};
|
||||
|
||||
if (!key.parseHex(request.at(JS(index)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedIndex"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedIndex"};
|
||||
}
|
||||
else if (request.contains(JS(account_root)))
|
||||
{
|
||||
if (!request.at(JS(account_root)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "account_rootNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "account_rootNotString"};
|
||||
|
||||
auto const account = ripple::parseBase58<ripple::AccountID>(
|
||||
request.at(JS(account_root)).as_string().c_str());
|
||||
if (!account || account->isZero())
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedAddress"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedAddress"};
|
||||
else
|
||||
key = ripple::keylet::account(*account).key;
|
||||
}
|
||||
else if (request.contains(JS(check)))
|
||||
{
|
||||
if (!request.at(JS(check)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "checkNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "checkNotString"};
|
||||
|
||||
if (!key.parseHex(request.at(JS(check)).as_string().c_str()))
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "checkMalformed"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "checkMalformed"};
|
||||
}
|
||||
}
|
||||
else if (request.contains(JS(deposit_preauth)))
|
||||
@@ -68,7 +69,8 @@ doLedgerEntry(Context const& context)
|
||||
request.at(JS(deposit_preauth)).as_string().c_str()))
|
||||
{
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, "deposit_preauthMalformed"};
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"deposit_preauthMalformed"};
|
||||
}
|
||||
}
|
||||
else if (
|
||||
@@ -78,7 +80,7 @@ doLedgerEntry(Context const& context)
|
||||
.at(JS(owner))
|
||||
.is_string())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
}
|
||||
else if (
|
||||
!request.at(JS(deposit_preauth))
|
||||
@@ -89,7 +91,8 @@ doLedgerEntry(Context const& context)
|
||||
.at(JS(authorized))
|
||||
.is_string())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "authorizedNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "authorizedNotString"};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -103,9 +106,11 @@ doLedgerEntry(Context const& context)
|
||||
deposit_preauth.at(JS(authorized)).as_string().c_str());
|
||||
|
||||
if (!owner)
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
else if (!authorized)
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedAuthorized"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedAuthorized"};
|
||||
else
|
||||
key = ripple::keylet::depositPreauth(*owner, *authorized).key;
|
||||
}
|
||||
@@ -115,18 +120,20 @@ doLedgerEntry(Context const& context)
|
||||
if (!request.at(JS(directory)).is_object())
|
||||
{
|
||||
if (!request.at(JS(directory)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "directoryNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "directoryNotString"};
|
||||
|
||||
if (!key.parseHex(request.at(JS(directory)).as_string().c_str()))
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedDirectory"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedDirectory"};
|
||||
}
|
||||
}
|
||||
else if (
|
||||
request.at(JS(directory)).as_object().contains(JS(sub_index)) &&
|
||||
!request.at(JS(directory)).as_object().at(JS(sub_index)).is_int64())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "sub_indexNotInt"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "sub_indexNotInt"};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -144,13 +151,14 @@ doLedgerEntry(Context const& context)
|
||||
{
|
||||
// May not specify both dir_root and owner.
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS,
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
"mayNotSpecifyBothDirRootAndOwner"};
|
||||
}
|
||||
else if (!uDirRoot.parseHex(
|
||||
directory.at(JS(dir_root)).as_string().c_str()))
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedDirRoot"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedDirRoot"};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -164,7 +172,8 @@ doLedgerEntry(Context const& context)
|
||||
|
||||
if (!ownerID)
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedAddress"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedAddress"};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -176,7 +185,7 @@ doLedgerEntry(Context const& context)
|
||||
else
|
||||
{
|
||||
return Status{
|
||||
Error::rpcINVALID_PARAMS, "missingOwnerOrDirRoot"};
|
||||
RippledError::rpcINVALID_PARAMS, "missingOwnerOrDirRoot"};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,19 +194,20 @@ doLedgerEntry(Context const& context)
|
||||
if (!request.at(JS(escrow)).is_object())
|
||||
{
|
||||
if (!key.parseHex(request.at(JS(escrow)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedEscrow"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedEscrow"};
|
||||
}
|
||||
else if (
|
||||
!request.at(JS(escrow)).as_object().contains(JS(owner)) ||
|
||||
!request.at(JS(escrow)).as_object().at(JS(owner)).is_string())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
}
|
||||
else if (
|
||||
!request.at(JS(escrow)).as_object().contains(JS(seq)) ||
|
||||
!request.at(JS(escrow)).as_object().at(JS(seq)).is_int64())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedSeq"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedSeq"};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -209,7 +219,8 @@ doLedgerEntry(Context const& context)
|
||||
.c_str());
|
||||
|
||||
if (!id)
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedAddress"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedAddress"};
|
||||
else
|
||||
{
|
||||
std::uint32_t seq =
|
||||
@@ -223,19 +234,20 @@ doLedgerEntry(Context const& context)
|
||||
if (!request.at(JS(offer)).is_object())
|
||||
{
|
||||
if (!key.parseHex(request.at(JS(offer)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedOffer"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedOffer"};
|
||||
}
|
||||
else if (
|
||||
!request.at(JS(offer)).as_object().contains(JS(account)) ||
|
||||
!request.at(JS(offer)).as_object().at(JS(account)).is_string())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedAccount"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedAccount"};
|
||||
}
|
||||
else if (
|
||||
!request.at(JS(offer)).as_object().contains(JS(seq)) ||
|
||||
!request.at(JS(offer)).as_object().at(JS(seq)).is_int64())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedSeq"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedSeq"};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -244,7 +256,8 @@ doLedgerEntry(Context const& context)
|
||||
offer.at(JS(account)).as_string().c_str());
|
||||
|
||||
if (!id)
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedAddress"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedAddress"};
|
||||
else
|
||||
{
|
||||
std::uint32_t seq =
|
||||
@@ -256,15 +269,18 @@ doLedgerEntry(Context const& context)
|
||||
else if (request.contains(JS(payment_channel)))
|
||||
{
|
||||
if (!request.at(JS(payment_channel)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "paymentChannelNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "paymentChannelNotString"};
|
||||
|
||||
if (!key.parseHex(request.at(JS(payment_channel)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedPaymentChannel"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedPaymentChannel"};
|
||||
}
|
||||
else if (request.contains(JS(ripple_state)))
|
||||
{
|
||||
if (!request.at(JS(ripple_state)).is_object())
|
||||
return Status{Error::rpcINVALID_PARAMS, "rippleStateNotObject"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "rippleStateNotObject"};
|
||||
|
||||
ripple::Currency currency;
|
||||
boost::json::object const& state =
|
||||
@@ -273,7 +289,7 @@ doLedgerEntry(Context const& context)
|
||||
if (!state.contains(JS(currency)) ||
|
||||
!state.at(JS(currency)).is_string())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedCurrency"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "currencyNotString"};
|
||||
}
|
||||
|
||||
if (!state.contains(JS(accounts)) ||
|
||||
@@ -284,7 +300,7 @@ doLedgerEntry(Context const& context)
|
||||
(state.at(JS(accounts)).as_array().at(0).as_string() ==
|
||||
state.at(JS(accounts)).as_array().at(1).as_string()))
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedAccounts"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedAccounts"};
|
||||
}
|
||||
|
||||
auto const id1 = ripple::parseBase58<ripple::AccountID>(
|
||||
@@ -293,11 +309,13 @@ doLedgerEntry(Context const& context)
|
||||
state.at(JS(accounts)).as_array().at(1).as_string().c_str());
|
||||
|
||||
if (!id1 || !id2)
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedAddresses"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedAddresses"};
|
||||
|
||||
else if (!ripple::to_currency(
|
||||
currency, state.at(JS(currency)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedCurrency"};
|
||||
return Status{
|
||||
ClioError::rpcMALFORMED_CURRENCY, "malformedCurrency"};
|
||||
|
||||
key = ripple::keylet::line(*id1, *id2, currency).key;
|
||||
}
|
||||
@@ -306,22 +324,25 @@ doLedgerEntry(Context const& context)
|
||||
if (!request.at(JS(ticket)).is_object())
|
||||
{
|
||||
if (!request.at(JS(ticket)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "ticketNotString"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "ticketNotString"};
|
||||
|
||||
if (!key.parseHex(request.at(JS(ticket)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedTicket"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedTicket"};
|
||||
}
|
||||
else if (
|
||||
!request.at(JS(ticket)).as_object().contains(JS(owner)) ||
|
||||
!request.at(JS(ticket)).as_object().at(JS(owner)).is_string())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
}
|
||||
else if (
|
||||
!request.at(JS(ticket)).as_object().contains(JS(ticket_seq)) ||
|
||||
!request.at(JS(ticket)).as_object().at(JS(ticket_seq)).is_int64())
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedTicketSeq"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedTicketSeq"};
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -333,7 +354,8 @@ doLedgerEntry(Context const& context)
|
||||
.c_str());
|
||||
|
||||
if (!id)
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "malformedOwner"};
|
||||
else
|
||||
{
|
||||
std::uint32_t seq = request.at(JS(offer))
|
||||
@@ -347,7 +369,7 @@ doLedgerEntry(Context const& context)
|
||||
}
|
||||
else
|
||||
{
|
||||
return Status{Error::rpcINVALID_PARAMS, "unknownOption"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "unknownOption"};
|
||||
}
|
||||
|
||||
auto dbResponse =
|
||||
|
||||
@@ -12,7 +12,7 @@ doLedgerRange(Context const& context)
|
||||
auto range = context.backend->fetchLedgerRange();
|
||||
if (!range)
|
||||
{
|
||||
return Status{Error::rpcNOT_READY, "rangeNotFound"};
|
||||
return Status{RippledError::rpcNOT_READY, "rangeNotFound"};
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -45,7 +45,8 @@ getURI(Backend::NFT const& dbResponse, Context const& context)
|
||||
|
||||
if (!blob || blob->size() == 0)
|
||||
return Status{
|
||||
Error::rpcINTERNAL, "Cannot find NFTokenPage for this NFT"};
|
||||
RippledError::rpcINTERNAL,
|
||||
"Cannot find NFTokenPage for this NFT"};
|
||||
|
||||
sle = ripple::STLedgerEntry(
|
||||
ripple::SerialIter{blob->data(), blob->size()}, nextKey);
|
||||
@@ -57,7 +58,7 @@ getURI(Backend::NFT const& dbResponse, Context const& context)
|
||||
|
||||
if (!sle)
|
||||
return Status{
|
||||
Error::rpcINTERNAL, "Cannot find NFTokenPage for this NFT"};
|
||||
RippledError::rpcINTERNAL, "Cannot find NFTokenPage for this NFT"};
|
||||
|
||||
auto const nfts = sle->getFieldArray(ripple::sfNFTokens);
|
||||
auto const nft = std::find_if(
|
||||
@@ -70,7 +71,7 @@ getURI(Backend::NFT const& dbResponse, Context const& context)
|
||||
|
||||
if (nft == nfts.end())
|
||||
return Status{
|
||||
Error::rpcINTERNAL, "Cannot find NFTokenPage for this NFT"};
|
||||
RippledError::rpcINTERNAL, "Cannot find NFTokenPage for this NFT"};
|
||||
|
||||
ripple::Blob const uriField = nft->getFieldVL(ripple::sfURI);
|
||||
|
||||
@@ -102,7 +103,7 @@ doNFTInfo(Context const& context)
|
||||
std::optional<Backend::NFT> dbResponse =
|
||||
context.backend->fetchNFT(tokenID, lgrInfo.seq, context.yield);
|
||||
if (!dbResponse)
|
||||
return Status{Error::rpcOBJECT_NOT_FOUND, "NFT not found"};
|
||||
return Status{RippledError::rpcOBJECT_NOT_FOUND, "NFT not found"};
|
||||
|
||||
response["nft_id"] = ripple::strHex(dbResponse->tokenID);
|
||||
response["ledger_index"] = dbResponse->ledgerSequence;
|
||||
|
||||
@@ -56,7 +56,7 @@ enumerateNFTOffers(
|
||||
// TODO: just check for existence without pulling
|
||||
if (!context.backend->fetchLedgerObject(
|
||||
directory.key, lgrInfo.seq, context.yield))
|
||||
return Status{Error::rpcOBJECT_NOT_FOUND, "notFound"};
|
||||
return Status{RippledError::rpcOBJECT_NOT_FOUND, "notFound"};
|
||||
|
||||
std::uint32_t limit;
|
||||
if (auto const status = getLimit(context, limit); status)
|
||||
@@ -78,10 +78,10 @@ enumerateNFTOffers(
|
||||
auto const& marker(request.at(JS(marker)));
|
||||
|
||||
if (!marker.is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "markerNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "markerNotString"};
|
||||
|
||||
if (!cursor.parseHex(marker.as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedCursor"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedCursor"};
|
||||
|
||||
auto const sle =
|
||||
read(ripple::keylet::nftoffer(cursor), lgrInfo, context);
|
||||
@@ -90,7 +90,7 @@ enumerateNFTOffers(
|
||||
sle->getFieldU16(ripple::sfLedgerEntryType) !=
|
||||
ripple::ltNFTOKEN_OFFER ||
|
||||
tokenid != sle->getFieldH256(ripple::sfNFTokenID))
|
||||
return Status{Error::rpcINVALID_PARAMS};
|
||||
return Status{RippledError::rpcINVALID_PARAMS};
|
||||
|
||||
startHint = sle->getFieldU64(ripple::sfNFTokenOfferNode);
|
||||
jsonOffers.push_back(json::value_from(*sle));
|
||||
|
||||
@@ -31,7 +31,8 @@ doNoRippleCheck(Context const& context)
|
||||
if (role == "gateway")
|
||||
roleGateway = true;
|
||||
else if (role != "user")
|
||||
return Status{Error::rpcINVALID_PARAMS, "role field is invalid"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "role field is invalid"};
|
||||
}
|
||||
|
||||
std::uint32_t limit = 300;
|
||||
|
||||
@@ -16,7 +16,7 @@ doServerInfo(Context const& context)
|
||||
if (!range)
|
||||
{
|
||||
return Status{
|
||||
Error::rpcNOT_READY,
|
||||
RippledError::rpcNOT_READY,
|
||||
"emptyDatabase",
|
||||
"The server has no data in the database"};
|
||||
}
|
||||
@@ -27,7 +27,7 @@ doServerInfo(Context const& context)
|
||||
auto fees = context.backend->fetchFees(lgrInfo->seq, context.yield);
|
||||
|
||||
if (!lgrInfo || !fees)
|
||||
return Status{Error::rpcINTERNAL};
|
||||
return Status{RippledError::rpcINTERNAL};
|
||||
|
||||
auto age = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
|
||||
@@ -22,10 +22,10 @@ validateStreams(boost::json::object const& request)
|
||||
auto const& stream : streams)
|
||||
{
|
||||
if (!stream.is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "streamNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "streamNotString"};
|
||||
|
||||
if (!validCommonStreams.contains(stream.as_string().c_str()))
|
||||
return Status{Error::rpcSTREAM_MALFORMED};
|
||||
return Status{RippledError::rpcSTREAM_MALFORMED};
|
||||
}
|
||||
|
||||
return OK;
|
||||
@@ -98,10 +98,10 @@ validateAccounts(boost::json::array const& accounts)
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
if (!account.is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "accountNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "accountNotString"};
|
||||
|
||||
if (!accountFromStringStrict(account.as_string().c_str()))
|
||||
return Status{Error::rpcACT_MALFORMED, "Account malformed."};
|
||||
return Status{RippledError::rpcACT_MALFORMED, "Account malformed."};
|
||||
}
|
||||
|
||||
return OK;
|
||||
@@ -212,7 +212,7 @@ validateAndGetBooks(
|
||||
std::shared_ptr<Backend::BackendInterface const> const& backend)
|
||||
{
|
||||
if (!request.at(JS(books)).is_array())
|
||||
return Status{Error::rpcINVALID_PARAMS, "booksNotArray"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "booksNotArray"};
|
||||
boost::json::array const& books = request.at(JS(books)).as_array();
|
||||
|
||||
std::vector<ripple::Book> booksToSub;
|
||||
@@ -294,7 +294,7 @@ doSubscribe(Context const& context)
|
||||
if (request.contains(JS(streams)))
|
||||
{
|
||||
if (!request.at(JS(streams)).is_array())
|
||||
return Status{Error::rpcINVALID_PARAMS, "streamsNotArray"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "streamsNotArray"};
|
||||
|
||||
auto status = validateStreams(request);
|
||||
|
||||
@@ -306,11 +306,11 @@ doSubscribe(Context const& context)
|
||||
{
|
||||
auto const& jsonAccounts = request.at(JS(accounts));
|
||||
if (!jsonAccounts.is_array())
|
||||
return Status{Error::rpcINVALID_PARAMS, "accountsNotArray"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "accountsNotArray"};
|
||||
|
||||
auto const& accounts = jsonAccounts.as_array();
|
||||
if (accounts.empty())
|
||||
return Status{Error::rpcACT_MALFORMED, "Account malformed."};
|
||||
return Status{RippledError::rpcACT_MALFORMED, "Account malformed."};
|
||||
|
||||
auto const status = validateAccounts(accounts);
|
||||
if (status)
|
||||
@@ -321,11 +321,12 @@ doSubscribe(Context const& context)
|
||||
{
|
||||
auto const& jsonAccounts = request.at(JS(accounts_proposed));
|
||||
if (!jsonAccounts.is_array())
|
||||
return Status{Error::rpcINVALID_PARAMS, "accountsProposedNotArray"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "accountsProposedNotArray"};
|
||||
|
||||
auto const& accounts = jsonAccounts.as_array();
|
||||
if (accounts.empty())
|
||||
return Status{Error::rpcACT_MALFORMED, "Account malformed."};
|
||||
return Status{RippledError::rpcACT_MALFORMED, "Account malformed."};
|
||||
|
||||
auto const status = validateAccounts(accounts);
|
||||
if (status)
|
||||
@@ -373,7 +374,7 @@ doUnsubscribe(Context const& context)
|
||||
if (request.contains(JS(streams)))
|
||||
{
|
||||
if (!request.at(JS(streams)).is_array())
|
||||
return Status{Error::rpcINVALID_PARAMS, "streamsNotArray"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "streamsNotArray"};
|
||||
|
||||
auto status = validateStreams(request);
|
||||
|
||||
@@ -385,11 +386,11 @@ doUnsubscribe(Context const& context)
|
||||
{
|
||||
auto const& jsonAccounts = request.at(JS(accounts));
|
||||
if (!jsonAccounts.is_array())
|
||||
return Status{Error::rpcINVALID_PARAMS, "accountsNotArray"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "accountsNotArray"};
|
||||
|
||||
auto const& accounts = jsonAccounts.as_array();
|
||||
if (accounts.empty())
|
||||
return Status{Error::rpcACT_MALFORMED, "Account malformed."};
|
||||
return Status{RippledError::rpcACT_MALFORMED, "Account malformed."};
|
||||
|
||||
auto const status = validateAccounts(accounts);
|
||||
if (status)
|
||||
@@ -400,11 +401,12 @@ doUnsubscribe(Context const& context)
|
||||
{
|
||||
auto const& jsonAccounts = request.at(JS(accounts_proposed));
|
||||
if (!jsonAccounts.is_array())
|
||||
return Status{Error::rpcINVALID_PARAMS, "accountsProposedNotArray"};
|
||||
return Status{
|
||||
RippledError::rpcINVALID_PARAMS, "accountsProposedNotArray"};
|
||||
|
||||
auto const& accounts = jsonAccounts.as_array();
|
||||
if (accounts.empty())
|
||||
return Status{Error::rpcACT_MALFORMED, "Account malformed."};
|
||||
return Status{RippledError::rpcACT_MALFORMED, "Account malformed."};
|
||||
|
||||
auto const status = validateAccounts(accounts);
|
||||
if (status)
|
||||
|
||||
@@ -14,7 +14,7 @@ doTransactionEntry(Context const& context)
|
||||
|
||||
ripple::uint256 hash;
|
||||
if (!hash.parseHex(getRequiredString(context.params, JS(tx_hash))))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedTransaction"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedTransaction"};
|
||||
|
||||
auto dbResponse = context.backend->fetchTransaction(hash, context.yield);
|
||||
// Note: transaction_entry is meant to only search a specified ledger for
|
||||
@@ -28,7 +28,7 @@ doTransactionEntry(Context const& context)
|
||||
// is in a different ledger than the one specified.
|
||||
if (!dbResponse || dbResponse->ledgerSequence != lgrInfo.seq)
|
||||
return Status{
|
||||
Error::rpcTXN_NOT_FOUND,
|
||||
RippledError::rpcTXN_NOT_FOUND,
|
||||
"transactionNotFound",
|
||||
"Transaction not found."};
|
||||
|
||||
|
||||
@@ -14,31 +14,31 @@ doTx(Context const& context)
|
||||
boost::json::object response = {};
|
||||
|
||||
if (!request.contains(JS(transaction)))
|
||||
return Status{Error::rpcINVALID_PARAMS, "specifyTransaction"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "specifyTransaction"};
|
||||
|
||||
if (!request.at(JS(transaction)).is_string())
|
||||
return Status{Error::rpcINVALID_PARAMS, "transactionNotString"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "transactionNotString"};
|
||||
|
||||
ripple::uint256 hash;
|
||||
if (!hash.parseHex(request.at(JS(transaction)).as_string().c_str()))
|
||||
return Status{Error::rpcINVALID_PARAMS, "malformedTransaction"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "malformedTransaction"};
|
||||
|
||||
bool binary = false;
|
||||
if (request.contains(JS(binary)))
|
||||
{
|
||||
if (!request.at(JS(binary)).is_bool())
|
||||
return Status{Error::rpcINVALID_PARAMS, "binaryFlagNotBool"};
|
||||
return Status{RippledError::rpcINVALID_PARAMS, "binaryFlagNotBool"};
|
||||
|
||||
binary = request.at(JS(binary)).as_bool();
|
||||
}
|
||||
|
||||
auto range = context.backend->fetchLedgerRange();
|
||||
if (!range)
|
||||
return Status{Error::rpcNOT_READY};
|
||||
return Status{RippledError::rpcNOT_READY};
|
||||
|
||||
auto dbResponse = context.backend->fetchTransaction(hash, context.yield);
|
||||
if (!dbResponse)
|
||||
return Status{Error::rpcTXN_NOT_FOUND};
|
||||
return Status{RippledError::rpcTXN_NOT_FOUND};
|
||||
|
||||
if (!binary)
|
||||
{
|
||||
|
||||
@@ -270,7 +270,7 @@ public:
|
||||
res.set(http::field::content_type, "application/json");
|
||||
res.keep_alive(req_.keep_alive());
|
||||
res.body() = boost::json::serialize(
|
||||
RPC::make_error(RPC::Error::rpcTOO_BUSY));
|
||||
RPC::makeError(RPC::RippledError::rpcTOO_BUSY));
|
||||
res.prepare_payload();
|
||||
lambda_(std::move(res));
|
||||
}
|
||||
@@ -375,7 +375,7 @@ handle_request(
|
||||
http::status::ok,
|
||||
"application/json",
|
||||
boost::json::serialize(
|
||||
RPC::make_error(RPC::Error::rpcBAD_SYNTAX))));
|
||||
RPC::makeError(RPC::RippledError::rpcBAD_SYNTAX))));
|
||||
}
|
||||
|
||||
auto range = backend->fetchLedgerRange();
|
||||
@@ -384,7 +384,7 @@ handle_request(
|
||||
http::status::ok,
|
||||
"application/json",
|
||||
boost::json::serialize(
|
||||
RPC::make_error(RPC::Error::rpcNOT_READY))));
|
||||
RPC::makeError(RPC::RippledError::rpcNOT_READY))));
|
||||
|
||||
std::optional<RPC::Context> context = RPC::make_HttpContext(
|
||||
yc,
|
||||
@@ -403,7 +403,7 @@ handle_request(
|
||||
http::status::ok,
|
||||
"application/json",
|
||||
boost::json::serialize(
|
||||
RPC::make_error(RPC::Error::rpcBAD_SYNTAX))));
|
||||
RPC::makeError(RPC::RippledError::rpcBAD_SYNTAX))));
|
||||
|
||||
boost::json::object response{{"result", boost::json::object{}}};
|
||||
boost::json::object& result = response["result"].as_object();
|
||||
@@ -418,7 +418,7 @@ handle_request(
|
||||
if (auto status = std::get_if<RPC::Status>(&v))
|
||||
{
|
||||
counters.rpcErrored(context->method);
|
||||
auto error = RPC::make_error(*status);
|
||||
auto error = RPC::makeError(*status);
|
||||
error["request"] = request;
|
||||
result = error;
|
||||
|
||||
@@ -438,16 +438,16 @@ handle_request(
|
||||
}
|
||||
|
||||
boost::json::array warnings;
|
||||
warnings.emplace_back(RPC::make_warning(RPC::warnRPC_CLIO));
|
||||
warnings.emplace_back(RPC::makeWarning(RPC::warnRPC_CLIO));
|
||||
auto lastCloseAge = context->etl->lastCloseAgeSeconds();
|
||||
if (lastCloseAge >= 60)
|
||||
warnings.emplace_back(RPC::make_warning(RPC::warnRPC_OUTDATED));
|
||||
warnings.emplace_back(RPC::makeWarning(RPC::warnRPC_OUTDATED));
|
||||
response["warnings"] = warnings;
|
||||
responseStr = boost::json::serialize(response);
|
||||
if (!dosGuard.add(ip, responseStr.size()))
|
||||
{
|
||||
response["warning"] = "load";
|
||||
warnings.emplace_back(RPC::make_warning(RPC::warnRPC_RATE_LIMIT));
|
||||
warnings.emplace_back(RPC::makeWarning(RPC::warnRPC_RATE_LIMIT));
|
||||
response["warnings"] = warnings;
|
||||
// reserialize when we need to include this warning
|
||||
responseStr = boost::json::serialize(response);
|
||||
@@ -462,7 +462,8 @@ handle_request(
|
||||
return send(httpResponse(
|
||||
http::status::internal_server_error,
|
||||
"application/json",
|
||||
boost::json::serialize(RPC::make_error(RPC::Error::rpcINTERNAL))));
|
||||
boost::json::serialize(
|
||||
RPC::makeError(RPC::RippledError::rpcINTERNAL))));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ public:
|
||||
|
||||
boost::json::object response = {};
|
||||
auto sendError = [this, &request, id](auto error) {
|
||||
auto e = RPC::make_error(error);
|
||||
auto e = RPC::makeError(error);
|
||||
if (!id.is_null())
|
||||
e["id"] = id;
|
||||
e["request"] = request;
|
||||
@@ -292,7 +292,7 @@ public:
|
||||
|
||||
auto range = backend_->fetchLedgerRange();
|
||||
if (!range)
|
||||
return sendError(RPC::Error::rpcNOT_READY);
|
||||
return sendError(RPC::RippledError::rpcNOT_READY);
|
||||
|
||||
std::optional<RPC::Context> context = RPC::make_WsContext(
|
||||
yield,
|
||||
@@ -311,7 +311,7 @@ public:
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning)
|
||||
<< tag() << " could not create RPC context";
|
||||
return sendError(RPC::Error::rpcBAD_SYNTAX);
|
||||
return sendError(RPC::RippledError::rpcBAD_SYNTAX);
|
||||
}
|
||||
|
||||
response = getDefaultWsResponse(id);
|
||||
@@ -327,7 +327,7 @@ public:
|
||||
{
|
||||
counters_.rpcErrored(context->method);
|
||||
|
||||
auto error = RPC::make_error(*status);
|
||||
auto error = RPC::makeError(*status);
|
||||
|
||||
if (!id.is_null())
|
||||
error["id"] = id;
|
||||
@@ -347,22 +347,22 @@ public:
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< tag() << __func__ << " caught exception : " << e.what();
|
||||
|
||||
return sendError(RPC::Error::rpcINTERNAL);
|
||||
return sendError(RPC::RippledError::rpcINTERNAL);
|
||||
}
|
||||
|
||||
boost::json::array warnings;
|
||||
|
||||
warnings.emplace_back(RPC::make_warning(RPC::warnRPC_CLIO));
|
||||
warnings.emplace_back(RPC::makeWarning(RPC::warnRPC_CLIO));
|
||||
|
||||
auto lastCloseAge = etl_->lastCloseAgeSeconds();
|
||||
if (lastCloseAge >= 60)
|
||||
warnings.emplace_back(RPC::make_warning(RPC::warnRPC_OUTDATED));
|
||||
warnings.emplace_back(RPC::makeWarning(RPC::warnRPC_OUTDATED));
|
||||
response["warnings"] = warnings;
|
||||
std::string responseStr = boost::json::serialize(response);
|
||||
if (!dosGuard_.add(*ip, responseStr.size()))
|
||||
{
|
||||
response["warning"] = "load";
|
||||
warnings.emplace_back(RPC::make_warning(RPC::warnRPC_RATE_LIMIT));
|
||||
warnings.emplace_back(RPC::makeWarning(RPC::warnRPC_RATE_LIMIT));
|
||||
response["warnings"] = warnings;
|
||||
// reserialize if we need to include this warning
|
||||
responseStr = boost::json::serialize(response);
|
||||
@@ -392,7 +392,7 @@ public:
|
||||
auto error,
|
||||
boost::json::value const& id,
|
||||
boost::json::object const& request) {
|
||||
auto e = RPC::make_error(error);
|
||||
auto e = RPC::makeError(error);
|
||||
|
||||
if (!id.is_null())
|
||||
e["id"] = id;
|
||||
@@ -417,14 +417,15 @@ public:
|
||||
|
||||
boost::json::object request;
|
||||
if (!raw.is_object())
|
||||
return sendError(RPC::Error::rpcINVALID_PARAMS, nullptr, request);
|
||||
return sendError(
|
||||
RPC::RippledError::rpcINVALID_PARAMS, nullptr, request);
|
||||
request = raw.as_object();
|
||||
|
||||
auto id = request.contains("id") ? request.at("id") : nullptr;
|
||||
|
||||
if (!dosGuard_.isOk(*ip))
|
||||
{
|
||||
sendError(RPC::Error::rpcSLOW_DOWN, id, request);
|
||||
sendError(RPC::RippledError::rpcSLOW_DOWN, id, request);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -438,7 +439,7 @@ public:
|
||||
shared_this->handle_request(std::move(r), id, yield);
|
||||
},
|
||||
dosGuard_.isWhiteListed(*ip)))
|
||||
sendError(RPC::Error::rpcTOO_BUSY, id, request);
|
||||
sendError(RPC::RippledError::rpcTOO_BUSY, id, request);
|
||||
}
|
||||
|
||||
do_read();
|
||||
|
||||
133
unittests/RPCErrors.cpp
Normal file
133
unittests/RPCErrors.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include <rpc/RPC.h>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace RPC;
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
void
|
||||
check(
|
||||
boost::json::object const& j,
|
||||
std::string_view error,
|
||||
uint32_t errorCode,
|
||||
std::string_view errorMessage)
|
||||
{
|
||||
EXPECT_TRUE(j.contains("error"));
|
||||
EXPECT_TRUE(j.contains("error_code"));
|
||||
EXPECT_TRUE(j.contains("error_message"));
|
||||
EXPECT_TRUE(j.contains("status"));
|
||||
EXPECT_TRUE(j.contains("type"));
|
||||
|
||||
EXPECT_TRUE(j.at("error").is_string());
|
||||
EXPECT_TRUE(j.at("error_code").is_uint64());
|
||||
EXPECT_TRUE(j.at("error_message").is_string());
|
||||
EXPECT_TRUE(j.at("status").is_string());
|
||||
EXPECT_TRUE(j.at("type").is_string());
|
||||
|
||||
EXPECT_STREQ(j.at("status").as_string().c_str(), "error");
|
||||
EXPECT_STREQ(j.at("type").as_string().c_str(), "response");
|
||||
|
||||
EXPECT_STREQ(j.at("error").as_string().c_str(), error.data());
|
||||
EXPECT_EQ(j.at("error_code").as_uint64(), errorCode);
|
||||
EXPECT_STREQ(
|
||||
j.at("error_message").as_string().c_str(), errorMessage.data());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(RPCErrorsTest, StatusAsBool)
|
||||
{
|
||||
// Only rpcSUCCESS status should return false
|
||||
EXPECT_FALSE(Status{RippledError::rpcSUCCESS});
|
||||
|
||||
// true should be returned for any error state, we just test a few
|
||||
CombinedError const errors[]{
|
||||
RippledError::rpcINVALID_PARAMS,
|
||||
RippledError::rpcUNKNOWN_COMMAND,
|
||||
RippledError::rpcTOO_BUSY,
|
||||
RippledError::rpcNO_NETWORK,
|
||||
RippledError::rpcACT_MALFORMED,
|
||||
RippledError::rpcBAD_MARKET,
|
||||
ClioError::rpcMALFORMED_CURRENCY,
|
||||
};
|
||||
|
||||
for (auto const& ec : errors)
|
||||
EXPECT_TRUE(Status{ec});
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, SuccessToJSON)
|
||||
{
|
||||
auto const status = Status{RippledError::rpcSUCCESS};
|
||||
check(makeError(status), "unknown", 0, "An unknown error code.");
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, RippledErrorToJSON)
|
||||
{
|
||||
auto const status = Status{RippledError::rpcINVALID_PARAMS};
|
||||
check(makeError(status), "invalidParams", 31, "Invalid parameters.");
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, RippledErrorFromStringToJSON)
|
||||
{
|
||||
auto const j = makeError(Status{"veryCustomError"});
|
||||
EXPECT_STREQ(j.at("error").as_string().c_str(), "veryCustomError");
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, RippledErrorToJSONCustomMessage)
|
||||
{
|
||||
auto const status = Status{RippledError::rpcINVALID_PARAMS, "custom"};
|
||||
check(makeError(status), "invalidParams", 31, "custom");
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, RippledErrorToJSONCustomStrCodeAndMessage)
|
||||
{
|
||||
auto const status =
|
||||
Status{RippledError::rpcINVALID_PARAMS, "customCode", "customMessage"};
|
||||
check(makeError(status), "customCode", 31, "customMessage");
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, ClioErrorToJSON)
|
||||
{
|
||||
auto const status = Status{ClioError::rpcMALFORMED_CURRENCY};
|
||||
check(makeError(status), "malformedCurrency", 5000, "Malformed currency.");
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, ClioErrorToJSONCustomMessage)
|
||||
{
|
||||
auto const status = Status{ClioError::rpcMALFORMED_CURRENCY, "custom"};
|
||||
check(makeError(status), "malformedCurrency", 5000, "custom");
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, ClioErrorToJSONCustomStrCodeAndMessage)
|
||||
{
|
||||
auto const status =
|
||||
Status{ClioError::rpcMALFORMED_CURRENCY, "customCode", "customMessage"};
|
||||
check(makeError(status), "customCode", 5000, "customMessage");
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, InvalidClioErrorToJSON)
|
||||
{
|
||||
EXPECT_ANY_THROW((void)makeError(static_cast<ClioError>(999999)));
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, WarningToJSON)
|
||||
{
|
||||
auto j = makeWarning(WarningCode::warnRPC_OUTDATED);
|
||||
EXPECT_TRUE(j.contains("id"));
|
||||
EXPECT_TRUE(j.contains("message"));
|
||||
|
||||
EXPECT_TRUE(j.at("id").is_int64());
|
||||
EXPECT_TRUE(j.at("message").is_string());
|
||||
|
||||
EXPECT_EQ(
|
||||
j.at("id").as_int64(),
|
||||
static_cast<uint32_t>(WarningCode::warnRPC_OUTDATED));
|
||||
EXPECT_STREQ(
|
||||
j.at("message").as_string().c_str(), "This server may be out of date");
|
||||
}
|
||||
|
||||
TEST(RPCErrorsTest, InvalidWarningToJSON)
|
||||
{
|
||||
EXPECT_ANY_THROW((void)makeWarning(static_cast<WarningCode>(999999)));
|
||||
}
|
||||
Reference in New Issue
Block a user