mirror of https://github.com/oxen-io/oxen-core.git
398 lines
15 KiB
C++
398 lines
15 KiB
C++
#include "core_rpc_server_command_parser.h"
|
|
#include "rpc/common/param_parser.hpp"
|
|
|
|
#include <chrono>
|
|
#include <oxenc/bt_serialize.h>
|
|
#include <oxenc/base64.h>
|
|
#include <oxenc/hex.h>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
namespace cryptonote::rpc {
|
|
|
|
using nlohmann::json;
|
|
|
|
void parse_request(ONS_RESOLVE& ons, rpc_input in) {
|
|
get_values(in,
|
|
"name_hash", required{ons.request.name_hash},
|
|
"type", required{ons.request.type});
|
|
}
|
|
|
|
void parse_request(GET_SERVICE_NODES& sns, rpc_input in) {
|
|
// Remember: key access must be in sorted order (even across get_values() calls).
|
|
get_values(in,
|
|
"active_only", sns.request.active_only);
|
|
|
|
bool fields_dict = false;
|
|
if (auto* json_in = std::get_if<json>(&in)) {
|
|
// Deprecated {"field":true, "field2":true, ...} handling:
|
|
if (auto fit = json_in->find("fields"); fit != json_in->end() && fit->is_object()) {
|
|
fields_dict = true;
|
|
for (auto& [k, v] : fit->items()) {
|
|
if (v.get<bool>()) {
|
|
if (k == "all") {
|
|
sns.request.fields.clear(); // Empty means all
|
|
break; // The old behaviour just ignored everything else if you specified all
|
|
}
|
|
sns.request.fields.insert(k);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!fields_dict) {
|
|
std::vector<std::string_view> fields;
|
|
get_values(in, "fields", fields);
|
|
for (const auto& f : fields)
|
|
sns.request.fields.emplace(f);
|
|
// If the only thing given is "all" then just clear it (as a small optimization):
|
|
if (sns.request.fields.size() == 1 && *sns.request.fields.begin() == "all")
|
|
sns.request.fields.clear();
|
|
}
|
|
|
|
get_values(in,
|
|
"limit", sns.request.limit,
|
|
"poll_block_hash", ignore_empty_string{sns.request.poll_block_hash},
|
|
"service_node_pubkeys", sns.request.service_node_pubkeys);
|
|
}
|
|
void parse_request(START_MINING& start_mining, rpc_input in) {
|
|
get_values(in,
|
|
"miner_address", required{start_mining.request.miner_address},
|
|
"num_blocks", start_mining.request.num_blocks,
|
|
"slow_mining", start_mining.request.slow_mining,
|
|
"threads_count", start_mining.request.threads_count);
|
|
}
|
|
void parse_request(GET_OUTPUTS& get_outputs, rpc_input in) {
|
|
get_values(in,
|
|
"as_tuple", get_outputs.request.as_tuple,
|
|
"get_txid", get_outputs.request.get_txid);
|
|
|
|
// "outputs" is trickier: for backwards compatibility we need to accept json of:
|
|
// [{"amount":0,"index":i1}, ...]
|
|
// but that is incredibly wasteful and so we also want the more efficient (and we only accept
|
|
// this for bt, since we don't have backwards compat to worry about):
|
|
// [i1, i2, ...]
|
|
bool legacy_outputs = false;
|
|
if (auto* json_in = std::get_if<json>(&in)) {
|
|
if (auto outputs = json_in->find("outputs");
|
|
outputs != json_in->end() && !outputs->empty() && outputs->is_array() && outputs->front().is_object()) {
|
|
legacy_outputs = true;
|
|
auto& reqoi = get_outputs.request.output_indices;
|
|
reqoi.reserve(outputs->size());
|
|
for (auto& o : *outputs)
|
|
reqoi.push_back(o["index"].get<uint64_t>());
|
|
}
|
|
}
|
|
if (!legacy_outputs)
|
|
get_values(in, "outputs", get_outputs.request.output_indices);
|
|
}
|
|
|
|
void parse_request(GET_TRANSACTION_POOL_STATS& pstats, rpc_input in) {
|
|
get_values(in, "include_unrelayed", pstats.request.include_unrelayed);
|
|
}
|
|
|
|
void parse_request(HARD_FORK_INFO& hfinfo, rpc_input in) {
|
|
get_values(in,
|
|
"height", hfinfo.request.height,
|
|
"version", hfinfo.request.version);
|
|
if (hfinfo.request.height && hfinfo.request.version)
|
|
throw std::runtime_error{"Error: at most one of 'height'" + std::to_string(hfinfo.request.height) + "/" + std::to_string(hfinfo.request.version) + " and 'version' may be specified"};
|
|
}
|
|
|
|
void parse_request(GET_TRANSACTIONS& get, rpc_input in) {
|
|
// Backwards compat for old stupid "txs_hashes" input name
|
|
if (auto* json_in = std::get_if<json>(&in))
|
|
if (auto it = json_in->find("txs_hashes"); it != json_in->end())
|
|
(*json_in)["tx_hashes"] = std::move(*it);
|
|
|
|
get_values(in,
|
|
"data", get.request.data,
|
|
"memory_pool", get.request.memory_pool,
|
|
"prune", get.request.prune,
|
|
"split", get.request.split,
|
|
"tx_extra", get.request.tx_extra,
|
|
"tx_extra_raw", get.request.tx_extra_raw,
|
|
"tx_hashes", get.request.tx_hashes);
|
|
|
|
if (get.request.memory_pool && !get.request.tx_hashes.empty())
|
|
throw std::runtime_error{"Error: 'memory_pool' and 'tx_hashes' are mutually exclusive"};
|
|
}
|
|
void parse_request(GET_TRANSACTION_POOL& get, rpc_input in) {
|
|
// Deprecated wrapper; GET_TRANSACTION_POOL is a no-member subclass of GET_TRANSACTIONS; it
|
|
// works identically, except that we force `memory_pool` to true.
|
|
parse_request(static_cast<GET_TRANSACTIONS&>(get), std::move(in));
|
|
if (!get.request.tx_hashes.empty())
|
|
throw std::runtime_error{"Error: 'get_transaction_pool' does not support specifying 'tx_hashes'"};
|
|
get.request.memory_pool = true;
|
|
}
|
|
|
|
void parse_request(SET_LIMIT& limit, rpc_input in) {
|
|
get_values(in,
|
|
"limit_down", limit.request.limit_down,
|
|
"limit_up", limit.request.limit_up);
|
|
|
|
if (limit.request.limit_down < -1)
|
|
throw std::domain_error{"limit_down must be >= -1"};
|
|
if (limit.request.limit_down < -1)
|
|
throw std::domain_error{"limit_up must be >= -1"};
|
|
}
|
|
|
|
void parse_request(IS_KEY_IMAGE_SPENT& spent, rpc_input in) {
|
|
get_values(in,
|
|
"key_images", spent.request.key_images);
|
|
}
|
|
|
|
void parse_request(SUBMIT_TRANSACTION& tx, rpc_input in) {
|
|
if (auto* json_in = std::get_if<json>(&in))
|
|
if (auto it = json_in->find("tx_as_hex"); it != json_in->end())
|
|
(*json_in)["tx"] = std::move(*it);
|
|
|
|
auto& tx_data = tx.request.tx;
|
|
get_values(in,
|
|
"blink", tx.request.blink,
|
|
"tx", required{tx_data});
|
|
|
|
if (tx_data.empty()) // required above will make sure it's specified, but doesn't guarantee against an empty value
|
|
throw std::domain_error{"Invalid 'tx' value: cannot be empty"};
|
|
|
|
// tx can be specified as base64, hex, or binary, so try to figure out which one we have by
|
|
// looking at the beginning.
|
|
//
|
|
// An encoded transaction always starts with the version byte, currently 0-4 (though 0 isn't
|
|
// actually used), with higher future values possible. That means in hex we get something like:
|
|
// `04...` and in base64 we get `B` (because the first 6 bits are 000001, and the b64 alphabet
|
|
// begins at `A` for 0). Thus the first bytes, for tx versions 0 through 48, are thus:
|
|
//
|
|
// binary: (31 binary control characters through 0x1f ... ) (space) ! " # $ % & ' ( ) * + , - . / 0
|
|
// base64: A A A A B B B B C C C C D D D D E E E E F F F F G G G G H H H H I I I I J J J J K K K K L L L L M
|
|
// hex: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3
|
|
//
|
|
// and so we run into the first ambiguity at version 48. Since we are currently only at version
|
|
// 4 (and Oxen started at version 2) this is likely to be sufficient for an extremely long time.
|
|
//
|
|
// Thus our heuristic:
|
|
// 'A'-'L' => base64
|
|
// '0'-'2' => hex
|
|
// \x00-\x2f => bytes
|
|
// anything else we reject as garbage.
|
|
auto tx0 = tx_data.front();
|
|
bool good = false;
|
|
if (tx0 <= 0x2f) {
|
|
good = true;
|
|
} else if (tx0 >= 'A' && tx0 <= 'L') {
|
|
if (oxenc::is_base64(tx_data)) {
|
|
auto end = oxenc::from_base64(tx_data.begin(), tx_data.end(), tx_data.begin());
|
|
tx_data.erase(end, tx_data.end());
|
|
good = true;
|
|
}
|
|
} else if (tx0 >= '0' && tx0 <= '2') {
|
|
if (oxenc::is_hex(tx_data)) {
|
|
auto end = oxenc::from_hex(tx_data.begin(), tx_data.end(), tx_data.begin());
|
|
tx_data.erase(end, tx_data.end());
|
|
good = true;
|
|
}
|
|
}
|
|
|
|
if (!good)
|
|
throw std::domain_error{"Invalid 'tx' value: expected hex, base64, or bytes"};
|
|
}
|
|
|
|
void parse_request(GET_BLOCK_HASH& bh, rpc_input in) {
|
|
get_values(in,
|
|
"heights", bh.request.heights);
|
|
|
|
if (bh.request.heights.size() > bh.MAX_HEIGHTS)
|
|
throw std::domain_error{"Error: too many block heights requested at once"};
|
|
}
|
|
|
|
void parse_request(GET_PEER_LIST& pl, rpc_input in) {
|
|
get_values(in,
|
|
"public_only", pl.request.public_only);
|
|
}
|
|
|
|
void parse_request(SET_LOG_LEVEL& set_log_level, rpc_input in) {
|
|
get_values(in,
|
|
"level", required{set_log_level.request.level});
|
|
}
|
|
|
|
void parse_request(SET_LOG_CATEGORIES& set_log_categories, rpc_input in) {
|
|
get_values(in,
|
|
"categories", required{set_log_categories.request.categories});
|
|
}
|
|
|
|
void parse_request(BANNED& banned, rpc_input in) {
|
|
get_values(in,
|
|
"address", required{banned.request.address});
|
|
}
|
|
|
|
void parse_request(FLUSH_TRANSACTION_POOL& flush_transaction_pool, rpc_input in) {
|
|
get_values(in,
|
|
"txids", flush_transaction_pool.request.txids);
|
|
}
|
|
|
|
void parse_request(GET_COINBASE_TX_SUM& get_coinbase_tx_sum, rpc_input in) {
|
|
get_values(in,
|
|
"count", get_coinbase_tx_sum.request.count,
|
|
"height", get_coinbase_tx_sum.request.height);
|
|
}
|
|
|
|
void parse_request(GET_BASE_FEE_ESTIMATE& get_base_fee_estimate, rpc_input in) {
|
|
get_values(in,
|
|
"grace_blocks", get_base_fee_estimate.request.grace_blocks);
|
|
}
|
|
|
|
void parse_request(OUT_PEERS& out_peers, rpc_input in){
|
|
get_values(in,
|
|
"out_peers", out_peers.request.out_peers,
|
|
"set", out_peers.request.set);
|
|
}
|
|
|
|
void parse_request(IN_PEERS& in_peers, rpc_input in){
|
|
get_values(in,
|
|
"in_peers", in_peers.request.in_peers,
|
|
"set", in_peers.request.set);
|
|
}
|
|
|
|
void parse_request(POP_BLOCKS& pop_blocks, rpc_input in){
|
|
get_values(in,
|
|
"nblocks", required{pop_blocks.request.nblocks});
|
|
}
|
|
|
|
void parse_request(LOKINET_PING& lokinet_ping, rpc_input in){
|
|
get_values(in,
|
|
"ed25519_pubkey", lokinet_ping.request.ed25519_pubkey,
|
|
"error", lokinet_ping.request.error,
|
|
"version", required{lokinet_ping.request.version});
|
|
}
|
|
|
|
void parse_request(STORAGE_SERVER_PING& storage_server_ping, rpc_input in){
|
|
get_values(in,
|
|
"ed25519_pubkey", required{storage_server_ping.request.ed25519_pubkey},
|
|
"error", storage_server_ping.request.error,
|
|
"https_port", required{storage_server_ping.request.https_port},
|
|
"omq_port", required{storage_server_ping.request.omq_port},
|
|
"version", required{storage_server_ping.request.version});
|
|
}
|
|
|
|
void parse_request(PRUNE_BLOCKCHAIN& prune_blockchain, rpc_input in){
|
|
get_values(in,
|
|
"check", prune_blockchain.request.check);
|
|
}
|
|
|
|
void parse_request(GET_SN_STATE_CHANGES& get_sn_state_changes, rpc_input in) {
|
|
get_values(in,
|
|
"end_height", get_sn_state_changes.request.end_height,
|
|
"start_height", required{get_sn_state_changes.request.start_height});
|
|
}
|
|
|
|
void parse_request(REPORT_PEER_STATUS& report_peer_status, rpc_input in) {
|
|
get_values(in,
|
|
"passed", report_peer_status.request.passed,
|
|
"pubkey", report_peer_status.request.pubkey,
|
|
"type", report_peer_status.request.type);
|
|
}
|
|
|
|
void parse_request(FLUSH_CACHE& flush_cache, rpc_input in) {
|
|
get_values(in,
|
|
"bad_blocks", flush_cache.request.bad_blocks,
|
|
"bad_txs", flush_cache.request.bad_txs);
|
|
}
|
|
|
|
void parse_request(GET_LAST_BLOCK_HEADER& get_last_block_header, rpc_input in) {
|
|
get_values(in,
|
|
"fill_pow_hash", get_last_block_header.request.fill_pow_hash,
|
|
"get_tx_hashes", get_last_block_header.request.get_tx_hashes);
|
|
}
|
|
|
|
void parse_request(GET_BLOCK_HEADER_BY_HASH& get_block_header_by_hash, rpc_input in) {
|
|
get_values(in,
|
|
"fill_pow_hash", get_block_header_by_hash.request.fill_pow_hash,
|
|
"get_tx_hashes", get_block_header_by_hash.request.get_tx_hashes,
|
|
"hash", get_block_header_by_hash.request.hash,
|
|
"hashes", get_block_header_by_hash.request.hashes);
|
|
}
|
|
|
|
void parse_request(SET_BANS& set_bans, rpc_input in) {
|
|
get_values(in,
|
|
"ban", required{set_bans.request.ban},
|
|
"host", required{set_bans.request.host},
|
|
"seconds", required{set_bans.request.seconds});
|
|
}
|
|
|
|
void parse_request(GET_STAKING_REQUIREMENT& get_staking_requirement, rpc_input in) {
|
|
get_values(in, "height", get_staking_requirement.request.height);
|
|
}
|
|
|
|
void parse_request(GET_BLOCK_HEADERS_RANGE& get_block_headers_range, rpc_input in) {
|
|
get_values(in,
|
|
"end_height", get_block_headers_range.request.end_height,
|
|
"fill_pow_hash", get_block_headers_range.request.fill_pow_hash,
|
|
"get_tx_hashes", get_block_headers_range.request.get_tx_hashes,
|
|
"start_height", get_block_headers_range.request.start_height);
|
|
}
|
|
|
|
void parse_request(GET_BLOCK_HEADER_BY_HEIGHT& get_block_header_by_height, rpc_input in) {
|
|
get_values(in,
|
|
"fill_pow_hash", get_block_header_by_height.request.fill_pow_hash,
|
|
"get_tx_hashes", get_block_header_by_height.request.get_tx_hashes,
|
|
"height", get_block_header_by_height.request.height,
|
|
"heights", get_block_header_by_height.request.heights);
|
|
}
|
|
|
|
void parse_request(GET_BLOCK& get_block, rpc_input in) {
|
|
get_values(in,
|
|
"fill_pow_hash", get_block.request.fill_pow_hash,
|
|
"hash", get_block.request.hash,
|
|
"height", get_block.request.height);
|
|
}
|
|
|
|
void parse_request(GET_OUTPUT_HISTOGRAM& get_output_histogram, rpc_input in) {
|
|
get_values(in,
|
|
"amounts", get_output_histogram.request.amounts,
|
|
"max_count", get_output_histogram.request.max_count,
|
|
"min_count", get_output_histogram.request.min_count,
|
|
"recent_cutoff", get_output_histogram.request.recent_cutoff,
|
|
"unlocked", get_output_histogram.request.unlocked);
|
|
}
|
|
|
|
void parse_request(GET_ACCRUED_BATCHED_EARNINGS& get_accrued_batched_earnings, rpc_input in) {
|
|
get_values(in,
|
|
"addresses", get_accrued_batched_earnings.request.addresses);
|
|
}
|
|
|
|
void parse_request(ONS_OWNERS_TO_NAMES& ons_owners_to_names, rpc_input in) {
|
|
get_values(in,
|
|
"entries", required{ons_owners_to_names.request.entries},
|
|
"include_expired", ons_owners_to_names.request.include_expired);
|
|
}
|
|
|
|
void parse_request(GET_QUORUM_STATE& qs, rpc_input in) {
|
|
|
|
get_values(in,
|
|
"end_height", qs.request.end_height,
|
|
"quorum_type", qs.request.quorum_type,
|
|
"start_height", qs.request.start_height);
|
|
|
|
if (qs.request.quorum_type) {
|
|
if (*qs.request.quorum_type == 255) // backwards-compat magic value
|
|
qs.request.quorum_type = std::nullopt;
|
|
else if (*qs.request.quorum_type > tools::enum_count<service_nodes::quorum_type>)
|
|
throw std::domain_error{
|
|
"Quorum type specifies an invalid value: "_format(*qs.request.quorum_type)};
|
|
}
|
|
}
|
|
|
|
void parse_request(GET_CHECKPOINTS& getcp, rpc_input in) {
|
|
get_values(in,
|
|
"count", getcp.request.count,
|
|
"end_height", getcp.request.end_height,
|
|
"start_height", getcp.request.start_height);
|
|
}
|
|
|
|
void parse_request(GET_SERVICE_NODE_REGISTRATION_CMD_RAW& cmd, rpc_input in) {
|
|
get_values(in,
|
|
"args", required{cmd.request.args},
|
|
"make_friendly", cmd.request.make_friendly,
|
|
"staking_requirement", required{cmd.request.staking_requirement});
|
|
}
|
|
}
|