Add ifelse branching metarequest

This lets you make conditional requests based on the HF, SS version, or
block height.  For future use (since this won't be usable before HF19).
This commit is contained in:
Jason Rhinelander 2022-05-09 20:39:46 -03:00
parent a4d93b9509
commit 389ac24d27
No known key found for this signature in database
GPG key ID: C4992CE7A88D4262
7 changed files with 521 additions and 29 deletions

View file

@ -0,0 +1,187 @@
from util import sn_address
import json
import ss
import time
from nacl.encoding import Base64Encoder
from nacl.hash import blake2b
m_yes = b'\x61\xeb'
b64_m_yes = 'Yes='
m_no = b'\x36\x8a\x5e'
b64_m_no = 'Nope'
def test_expire_all(omq, random_sn, sk, exclude):
swarm = ss.get_swarm(omq, random_sn, sk)
sn = ss.random_swarm_members(swarm, 1, exclude)[0]
conn = omq.connect_remote(sn_address(sn))
ts = int(time.time() * 1000)
ttl = 86_400_000
my_ss_id = '05' + sk.verify_key.encode().hex()
def store_action(msg, ts):
return {
'method': 'store',
'params': {
'pubkey': my_ss_id,
'timestamp': ts,
'ttl': ttl,
'data': msg
}
}
r = []
r.append(omq.request_future(conn, 'storage.ifelse',
[json.dumps({
'if': { 'hf_at_least': [2000000] },
'then': store_action(b64_m_yes, ts),
'else': store_action(b64_m_no, ts),
})]))
r.append(omq.request_future(conn, 'storage.ifelse',
[json.dumps({
'if': { 'hf_at_least': [19], 'height_before': 123456 },
'then': store_action(b64_m_yes, ts+1),
'else': store_action(b64_m_no, ts+1),
})]))
r.append(omq.request_future(conn, 'storage.ifelse',
[json.dumps({
'if': { 'hf_at_least': [19], 'height_before': 123456789 },
'then': store_action(b64_m_yes, ts+2),
'else': store_action(b64_m_no, ts+2),
})]))
r.append(omq.request_future(conn, 'storage.ifelse',
[json.dumps({
'if': { 'hf_at_least': [19] },
'then': store_action(b64_m_yes, ts+3),
'else': store_action(b64_m_no, ts+3),
})]))
r.append(omq.request_future(conn, 'storage.ifelse',
[json.dumps({
'if': { 'hf_at_least': [19, 1] },
'then': store_action(b64_m_yes, ts+4),
})]))
r.append(omq.request_future(conn, 'storage.ifelse',
[json.dumps({
'if': { 'hf_before': [19] },
'then': store_action(b64_m_yes, ts+5),
})]))
r.append(omq.request_future(conn, 'storage.ifelse',
[json.dumps({
'if': { 'hf_at_least': [19] },
'else': store_action(b64_m_yes, ts+6),
})]))
r.append(omq.request_future(conn, 'storage.ifelse',
[json.dumps({
'if': { 'hf_before': [19] },
'else': store_action(b64_m_no, ts+7),
})]))
r.append(omq.request_future(conn, 'storage.ifelse',
[json.dumps({
'if': { 'hf_at_least': [19] },
'then': {
'method': 'ifelse',
'params': { 'if': { 'hf_at_least': [19] }, 'then': {
'method': 'ifelse',
'params': { 'if': { 'height_at_least': 100 }, 'then': {
'method': 'ifelse',
'params': { 'if': { 'v_at_least': [2, 2] }, 'then': {
'method': 'ifelse',
'params': {
'if': { 'hf_before': [99999, 99] },
'then': {
'method': 'batch',
'params': {
'requests': [
store_action(b64_m_yes, ts+8),
store_action(b64_m_yes, ts+9),
store_action(b64_m_yes, ts+10)
]
}
}
}
}}
}}
}}
}
})]))
bad = omq.request_future(conn, 'storage.batch',
[json.dumps({
'requests': [{
'method': 'ifelse',
'params': {
'if': { 'hf_at_least': [19] },
'then': { 'method': 'info', 'params': {} },
'else': { 'method': 'info', 'params': {} }
}
}]
})])
def hash(body, ts):
return blake2b(f"{ts}{ts+ttl}".encode() + b'\x05' + sk.verify_key.encode() + body,
encoder=Base64Encoder).decode().rstrip('=')
for i in range(len(r)):
r[i] = r[i].get()
print(r[i])
assert len(r[i]) == 1
r[i] = json.loads(r[i][0])
if i not in (5, 6):
assert 'result' in r[i] and r[i]['result']['code'] == 200 and 'body' in r[i]['result']
assert not r[0]['condition']
assert r[0]['result']['body']['hash'] == hash(m_no, ts)
assert not r[1]['condition']
assert r[1]['result']['body']['hash'] == hash(m_no, ts+1)
assert r[2]['condition']
assert r[2]['result']['body']['hash'] == hash(m_yes, ts+2)
assert r[3]['condition']
assert r[3]['result']['body']['hash'] == hash(m_yes, ts+3)
assert r[4]['condition']
assert r[4]['result']['body']['hash'] == hash(m_yes, ts+4)
assert not r[5]['condition']
assert 'result' not in r[5]
assert r[6]['condition']
assert 'result' not in r[6]
assert not r[7]['condition']
assert r[7]['result']['body']['hash'] == hash(m_no, ts+7)
x = r[8]
assert x['condition'] # hf >= 19
assert x['result']['code'] == 200
x = x['result']['body']
assert x['condition'] # hf >= 19
assert x['result']['code'] == 200
x = x['result']['body']
assert x['condition'] # height >= 100
assert x['result']['code'] == 200
x = x['result']['body']
assert x['condition'] # v >= 2.2
assert x['result']['code'] == 200
x = x['result']['body']
assert x['condition'] # hf < 99999.99
assert x['result']['code'] == 200
x = x['result']['body']
x = x['results']
assert len(x) == 3
assert [y['code'] for y in x] == [200, 200, 200]
assert x[0]['body']['hash'] == hash(m_yes, ts+8)
assert x[1]['body']['hash'] == hash(m_yes, ts+9)
assert x[2]['body']['hash'] == hash(m_yes, ts+10)

View file

@ -20,6 +20,12 @@ struct type_list_append<type_list<T...>, S...> {
template <typename... T>
using type_list_append_t = typename type_list_append<T...>::type;
template <typename...>
constexpr bool type_list_contains = false;
template <typename... S, typename T>
inline constexpr bool type_list_contains<T, type_list<S...>> = (std::is_same_v<T, S> || ...);
/// Helper for converting a type_list<T...> into a std::variant<T...>. (Note that std::variant
/// requires at least one T).
template <typename... T>

View file

@ -3,6 +3,7 @@
#include <oxenss/logging/oxen_logger.h>
#include <oxenss/utils/string_utils.hpp>
#include <oxenss/utils/time.hpp>
#include <oxenss/version.h>
#include <chrono>
#include <limits>
@ -28,6 +29,8 @@ namespace {
template <typename T>
constexpr bool is_str_array = std::is_same_v<T, std::vector<std::string>>;
template <typename T>
constexpr bool is_int_array = std::is_same_v<T, std::vector<int>>;
template <typename T>
constexpr bool is_namespace_var = std::is_same_v<T, namespace_var>;
template <typename T>
@ -38,12 +41,13 @@ namespace {
: std::is_same_v<T, namespace_id> ? "16-bit integer"sv
: is_timestamp<T> ? "integer timestamp (in milliseconds)"sv
: is_str_array<T> ? "string array"sv
: is_int_array<T> ? "integer array"sv
: "string"sv;
template <typename T>
constexpr bool is_parseable_v =
std::is_unsigned_v<T> || std::is_integral_v<T> || is_timestamp<T> || is_str_array<T> ||
is_namespace_var<T> || std::is_same_v<T, std::string_view> ||
is_int_array<T> || is_namespace_var<T> || std::is_same_v<T, std::string_view> ||
std::is_same_v<T, std::string> || std::is_same_v<T, namespace_id>;
// Extracts a field suitable for a `T` value from the given json with name `name`. Takes
@ -62,12 +66,18 @@ namespace {
: std::is_integral_v<T> || std::is_same_v<T, namespace_id>
? it->is_number_integer()
: is_namespace_var<T> ? it->is_number_integer() || it->is_string()
: is_str_array<T> ? it->is_array()
: it->is_string();
if (is_str_array<T> && right_type)
: is_str_array<T> || is_int_array<T> ? it->is_array()
: it->is_string();
if (is_str_array<T> && right_type) {
for (auto& x : *it)
if (!x.is_string())
right_type = false;
} else if (is_int_array<T> && right_type) {
for (auto& x : *it)
if (!x.is_number_integer())
right_type = false;
}
if (!right_type)
throw parse_error{
fmt::format("Invalid value given for '{}': expected {}", name, type_desc<T>)};
@ -117,11 +127,14 @@ namespace {
return params.consume_integer<T>();
else if constexpr (is_timestamp<T>)
return from_epoch_ms(params.consume_integer<int64_t>());
else if constexpr (is_str_array<T>) {
auto strs = std::make_optional<T>();
else if constexpr (is_str_array<T> || is_int_array<T>) {
auto elems = std::make_optional<T>();
for (auto l = params.consume_list_consumer(); !l.is_finished();)
strs->push_back(l.consume_string());
return strs;
if constexpr (is_str_array<T>)
elems->push_back(l.consume_string());
else
elems->push_back(l.consume_integer<int>());
return elems;
} else if constexpr (is_namespace_var<T> || std::is_same_v<T, namespace_id>) {
if (params.is_integer())
return namespace_id{
@ -689,6 +702,19 @@ void oxend_request::load_from(bt_dict_consumer params) {
load(*this, params);
}
static client_subrequest as_subrequest(client_request&& req) {
return var::visit(
[](auto&& r) -> client_subrequest {
using T = std::decay_t<decltype(r)>;
if constexpr (type_list_contains<T, client_rpc_subrequests>)
return std::move(r);
else
throw parse_error{
"Invalid batch subrequest: subrequests may not contain meta-requests"};
},
std::move(req));
}
void batch::load_from(json params) {
auto reqs_it = params.find("requests");
if (reqs_it == params.end() || !reqs_it->is_array() || reqs_it->empty())
@ -706,11 +732,10 @@ void batch::load_from(json params) {
throw parse_error{"Invalid batch request: subrequests must have method/params keys"};
auto method = meth_it->get<std::string_view>();
auto rpc_it = RequestHandler::client_rpc_endpoints.find(method);
if (rpc_it == RequestHandler::client_rpc_endpoints.end() ||
!rpc_it->second.load_subreq_json)
if (rpc_it == RequestHandler::client_rpc_endpoints.end())
throw parse_error{
"Invalid batch subrequest: invalid method \"" + std::string{method} + "\""};
subreqs.push_back(rpc_it->second.load_subreq_json(std::move(*params_it)));
subreqs.push_back(as_subrequest(rpc_it->second.load_req(std::move(*params_it))));
}
}
void batch::load_from(bt_dict_consumer params) {
@ -728,15 +753,147 @@ void batch::load_from(bt_dict_consumer params) {
throw parse_error{"Invalid batch request: subrequests must have a method"};
auto method = sr.consume_string_view();
auto rpc_it = RequestHandler::client_rpc_endpoints.find(method);
if (rpc_it == RequestHandler::client_rpc_endpoints.end() || !rpc_it->second.load_subreq_bt)
if (rpc_it == RequestHandler::client_rpc_endpoints.end())
throw parse_error{
"Invalid batch subrequest: invalid method \"" + std::string{method} + "\""};
if (!sr.skip_until("params") || !sr.is_dict())
throw parse_error{"Invalid batch request: subrequests must have a params dict"};
subreqs.push_back(rpc_it->second.load_subreq_bt(sr.consume_dict_consumer()));
subreqs.push_back(as_subrequest(rpc_it->second.load_req(sr.consume_dict_consumer())));
}
if (subreqs.empty())
throw parse_error{"Invalid batch request: empty \"requests\" list"};
}
// Copies an optional vector into a fixed-size array, substituting 0's for omitted vector elements,
// and ignoring anything in the vector longer than the given size. Gives nullopt if the input
// vector is itself nullopt or empty.
template <size_t N, typename T>
static std::optional<std::array<T, N>> to_fixed_array(const std::optional<std::vector<T>>& in) {
if (!in || in->empty())
return std::nullopt;
std::array<T, N> out;
for (size_t i = 0; i < N; i++)
out[i] = i < in->size() ? (*in)[i] : T{0};
return out;
}
template <typename Dict>
static void load_condition(ifelse& i, Dict if_) {
auto [height_ge_, height_lt_, hf_ge_, hf_lt_, v_ge_, v_lt_] = load_fields<
int,
int,
std::vector<int>,
std::vector<int>,
std::vector<int>,
std::vector<int>>(
if_,
"height_at_least",
"height_before",
"hf_at_least",
"hf_before",
"v_at_least",
"v_before");
auto hf_ge = to_fixed_array<2>(hf_ge_);
auto hf_lt = to_fixed_array<2>(hf_lt_);
auto v_ge = to_fixed_array<3>(v_ge_);
auto v_lt = to_fixed_array<3>(v_lt_);
auto height_ge = height_ge_;
auto height_lt = height_lt_;
if (!(height_ge_ || height_lt_ || hf_ge || hf_lt || v_ge || v_lt))
throw parse_error{"Invalid ifelse request: must specify at least one \"if\" condition"};
i.condition = [=](const snode::ServiceNode& snode) {
bool result = true;
if (hf_ge || hf_lt) {
std::array<int, 2> hf = {snode.hf().first, snode.hf().second};
if (hf_ge)
result &= hf >= *hf_ge;
if (hf_lt)
result &= hf < *hf_lt;
}
if (v_ge || v_lt) {
std::array<int, 3> v = {
STORAGE_SERVER_VERSION[0],
STORAGE_SERVER_VERSION[1],
STORAGE_SERVER_VERSION[2]};
if (v_ge)
result &= v >= *v_ge;
if (v_lt)
result &= v < *v_lt;
}
if (height_ge || height_lt) {
auto height = static_cast<int>(snode.blockheight());
if (height_ge)
result &= height >= *height_ge;
if (height_lt)
result &= height < *height_lt;
}
return result;
};
}
static std::unique_ptr<client_request> load_ifelse_request(json& params, const std::string& key) {
auto it = params.find(key);
if (it == params.end())
return nullptr;
if (!it->is_object())
throw parse_error{"Invalid ifelse request: " + key + " must be an object"};
auto mit = it->find("method");
auto pit = it->find("params");
if (mit == it->end() || !mit->is_string() || pit == it->end())
throw parse_error{"Invalid ifelse request: " + key + " must have method/params keys"};
auto method = mit->get<std::string_view>();
auto rpc_it = RequestHandler::client_rpc_endpoints.find(method);
if (rpc_it == RequestHandler::client_rpc_endpoints.end())
throw parse_error{"Invalid ifelse request method \"" + key + "\""};
return var::visit(
[](auto&& r) { return std::make_unique<client_request>(std::move(r)); },
rpc_it->second.load_req(std::move(*pit)));
}
static std::unique_ptr<client_request> load_ifelse_request(
bt_dict_consumer& params, const std::string& key) {
if (!params.skip_until(key))
return nullptr;
if (!params.is_dict())
throw parse_error{"Invalid ifelse request: " + key + " must be a dict"};
auto req = params.consume_dict_consumer();
if (!req.skip_until("method") || !req.is_string())
throw parse_error{"Invalid ifelse request: " + key + " missing method"};
auto method = req.consume_string_view();
auto rpc_it = RequestHandler::client_rpc_endpoints.find(method);
if (rpc_it == RequestHandler::client_rpc_endpoints.end())
throw parse_error{"Invalid ifelse request method \"" + key + "\""};
if (!req.skip_until("params") || !req.is_dict())
throw parse_error{"Invalid ifelse request: " + key + " missing params"};
return var::visit(
[](auto&& r) { return std::make_unique<client_request>(std::move(r)); },
rpc_it->second.load_req(req.consume_dict_consumer()));
}
void ifelse::load_from(json params) {
auto cond_it = params.find("if");
if (cond_it == params.end() || !cond_it->is_object())
throw parse_error{"Invalid ifelse request: no valid \"if\" field"};
load_condition(*this, std::move(*cond_it));
action_true = load_ifelse_request(params, "then");
action_false = load_ifelse_request(params, "else");
if (!action_true && !action_false)
throw parse_error{"Invalid ifelse request: at least one of \"then\"/\"else\" required"};
}
void ifelse::load_from(bt_dict_consumer params) {
action_false = load_ifelse_request(params, "else");
if (!params.skip_until("if") || !params.is_dict())
throw parse_error{"Invalid ifelse request: no valid \"if\" field"};
load_condition(*this, params.consume_dict_consumer());
action_true = load_ifelse_request(params, "then");
if (!action_true && !action_false)
throw parse_error{"Invalid ifelse request: at least one of \"then\"/\"else\" required"};
}
} // namespace oxen::rpc

View file

@ -16,6 +16,10 @@
#include <oxenss/common/namespace.h>
#include <oxenss/common/type_list.h>
namespace oxen::snode {
class ServiceNode;
}
namespace oxen::rpc {
using namespace std::literals;
@ -515,7 +519,8 @@ struct oxend_request final : endpoint {
};
// All of the RPC types that can be invoked as a regular request: either directly, or inside a
// batch. (This is everything except batch itself, because batch does not permit recursion).
// batch. This excludes the meta-requests like batch/sequence/ifelse (since those nest other
// requests within them).
using client_rpc_subrequests = type_list<
store,
retrieve,
@ -603,8 +608,117 @@ struct sequence : batch {
static constexpr auto names() { return NAMES("sequence"); }
};
struct ifelse;
// All of the RPC types that can be invoked as top-level requests, i.e. all of the subrequest types
// plus batch and sequence. These are loaded into the supported RPC interfaces at startup.
using client_rpc_types = type_list_append_t<client_rpc_subrequests, batch, sequence>;
// (which are invokable via batch/sequence), plus batch, sequence, and ifelse (which are not
// batch-invokable). These are loaded into the supported RPC interfaces at startup.
using client_rpc_types = type_list_append_t<client_rpc_subrequests, batch, sequence, ifelse>;
using client_request = type_list_variant_t<client_rpc_types>;
/// Conditional request: this endpoints allows you to invoke a request dependent on the storage
/// server and/or current hardfork version.
///
/// This endpoint takes a dict parameter containing three keys:
///
/// - An `"if"` key contains a dict of conditions to check; this dict has keys:
/// - `"hf_at_least"` -- contains a two-element list of hardfork/softfork revisions, e.g. [19,1].
/// The "yes" endpoint will be invoked if this is true.
/// - `"v_at_least"` -- contains a three-element list of the storage server major/minor/patch
/// versions, e.g. [2,3,0]. The "yes" endpoint will be invoked if this is true.
/// - `"height_at_least"` -- contains a blockchain height (integer); the "yes" branch will be
/// executed if the current blockchain height is at least the given value.
/// - `"hf_before"`, `"hf_before"`, `"height_before"` -- negations of the above "..._at_least"
/// conditions. e.g. `"hf_at_least": [19,1]` and `"hf_before": [19,1]` follow the opposite
/// branches.
///
/// If more than one key is specified then all given keys must be satisfied to pass the condition.
/// (That is: conditions are "and"ed together).
///
/// - A `"then"` key contains a single request to invoke if the condition is satisfied. The request
/// itself is specified as a dict containing "method" and "params" keys containing the endpoint to
/// invoke and the parameters to pass to the request. The given request is permitted to be
/// a nested "ifelse" or a "batch"/"sequence". Note, however, that batch/sequence requests may
/// not contain "ifelse" requests.
///
/// - An `"else"` key contains a single request to invoke if the condition is *not* satisfied.
/// Parameters are the same as `"then"`.
///
/// `"if"` is always required, and at least one of "then" and "else" is required: if one or the
/// other is omitted then no action is performed if that branch would be followed.
///
/// This endpoint returns a dict containing keys:
/// - "hf" -- the current hardfork version (e.g. [19,1])
/// - "v" -- the running storage server version (e.g. [2,3,0])
/// - "height" -- the current blockchain height (e.g. 1234567)
/// - "condition" -- true or false indicating the logical result of the `"if"` condition.
/// - "result" -- a dict containing the result of the logical branch ("then" or "else") that was
/// followed. This dict has two keys:
/// - "code" -- the numeric response code (e.g. 200 for a typical success)
/// - "body" -- the response value (usually a dict).
/// If the branch followed was omitted from the request (e.g. the condition failed and only a
/// "then" branch was given) then this "result" key is omitted entirely.
///
/// Example:
///
/// Suppose HF 19.2 introduces some fancy new command "abcd" but earlier versions require executing
/// a pair of commands "ab" and "cd" to get the same effect:
///
/// Request:
///
/// {
/// "if": { "hf_at_least": [19,2] },
/// "then": { "method": "abcd", "params": { "z": 1 } },
/// "else": {
/// "method": "batch",
/// "params": {
/// "requests": [
/// {"method": "ab", "params": {"z": 1}},
/// {"method": "cd", "params": {"z": 3}}
/// ]
/// }
/// }
/// }
///
/// If the 19.2 hf is active then the response would be:
///
/// {
/// "hf": [19,2],
/// "v": [2,3,1],
/// "height": 1234567,
/// "condition": true,
/// "result": { "code": 200, "body": {"z_plus_4": 5}}
/// }
///
/// Response from some blockchain height before hf 19.2:
///
/// {
/// "hf": [19,1],
/// "v": [2,3,1],
/// "height": 1230000,
/// "condition": false,
/// "result": {
/// "code": 200,
/// "body": [
/// {"code": 200, "body": {"z_plus_2": 3}},
/// {"code": 200, "body": {"z_plus_2": 5}}
/// ]
/// }
/// }
///
struct ifelse : endpoint {
static constexpr auto names() { return NAMES("ifelse"); }
std::function<bool(const snode::ServiceNode& snode)> condition;
// We're effectively using these like an std::optional, but we need pointer indirection because
// we can potentially self-reference (and can't do that with an optional because we haven't
// fully defined our own type yet).
std::unique_ptr<client_request> action_true;
std::unique_ptr<client_request> action_false;
void load_from(nlohmann::json params) override;
void load_from(oxenc::bt_dict_consumer params) override;
};
} // namespace oxen::rpc

View file

@ -102,14 +102,11 @@ namespace {
template <typename RPC>
void register_client_rpc_endpoint(RequestHandler::rpc_map& regs) {
RequestHandler::rpc_handler calls;
if constexpr (!std::is_base_of_v<batch, RPC>) {
calls.load_subreq_json = [](json params) -> client_subrequest {
return load_request<RPC>(std::move(params));
};
calls.load_subreq_bt = [](oxenc::bt_dict_consumer params) -> client_subrequest {
return load_request<RPC>(std::move(params));
};
}
calls.load_req = [](std::variant<json, oxenc::bt_dict_consumer> params) -> client_request {
return std::visit(
[](auto&& params) { return load_request<RPC>(std::move(params)); },
std::move(params));
};
calls.http_json = [](RequestHandler& h, json params, std::function<void(Response)> cb) {
auto req = load_request<RPC>(std::move(params));
h.process_client_req(std::move(req), std::move(cb));
@ -1088,7 +1085,7 @@ void RequestHandler::process_client_req(rpc::batch&& req, std::function<void(rpc
if (done)
cb(Response{http::OK, json({{"results", std::move(*subresults)}})});
};
std::visit(
var::visit(
[this, handler = std::move(handler)](auto&& s) {
process_client_req(std::move(s), std::move(handler));
},
@ -1144,7 +1141,7 @@ void RequestHandler::process_client_req(
cb(Response{http::OK, json({{"results", std::move(manager->subresults)}})});
} else {
// subrequest was successful and we're not done, so fire off the next one
std::visit(
var::visit(
[&](auto&& subreq) {
process_client_req(std::move(subreq), manager->subresult_callback);
},
@ -1152,13 +1149,41 @@ void RequestHandler::process_client_req(
}
};
std::visit(
var::visit(
[&](auto&& subreq) {
process_client_req(std::move(subreq), manager->subresult_callback);
},
manager->subreqs[0]);
}
void RequestHandler::process_client_req(rpc::ifelse&& req, std::function<void(rpc::Response)> cb) {
bool cond = req.condition(service_node_);
json response{
{"hf", service_node_.hf()},
{"v", STORAGE_SERVER_VERSION},
{"height", service_node_.blockheight()},
{"condition", cond}};
auto& subreq = cond ? req.action_true : req.action_false;
if (!subreq) // No subrequest action for this branch
return cb(Response{http::OK, std::move(response)});
auto wrap_response = [response = std::move(response),
cb = std::move(cb)](rpc::Response r) mutable {
response["result"] = json{{"code", r.status.first}};
if (auto* j = std::get_if<json>(&r.body))
response["result"]["body"] = std::move(*j);
else
response["result"]["body"] = std::string{view_body(r)};
cb(Response{http::OK, std::move(response)});
};
var::visit(
[&](auto&& subreq) { process_client_req(std::move(subreq), std::move(wrap_response)); },
std::move(*subreq));
}
void RequestHandler::process_client_req(
std::string_view req_json, std::function<void(Response)> cb) {
OXEN_LOG(trace, "process_client_req str <{}>", req_json);

View file

@ -193,10 +193,11 @@ class RequestHandler {
void process_client_req(rpc::expire_msgs&&, std::function<void(Response)> cb);
void process_client_req(rpc::batch&&, std::function<void(Response)> cb);
void process_client_req(rpc::sequence&&, std::function<void(Response)> cb);
void process_client_req(rpc::ifelse&&, std::function<void(Response)> cb);
struct rpc_handler {
std::function<client_subrequest(nlohmann::json params)> load_subreq_json;
std::function<client_subrequest(oxenc::bt_dict_consumer params)> load_subreq_bt;
std::function<client_request(std::variant<nlohmann::json, oxenc::bt_dict_consumer> params)>
load_req;
std::function<void(RequestHandler&, nlohmann::json, std::function<void(Response)>)>
http_json;
std::function<void(

View file

@ -199,6 +199,8 @@ class ServiceNode {
const hf_revision& hf() const { return hardfork_; }
const uint64_t& blockheight() const { return block_height_; }
bool hf_at_least(hf_revision version) const { return hardfork_ >= version; }
// Return true if the service node is ready to handle requests, which means the storage