Files
triangles_v5/src/rest.cpp
T
2026-05-10 02:33:40 -07:00

922 lines
32 KiB
C++

// Copyright (c) 2024 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "rest.h"
#include "trianglesrpc.h"
#include "main.h"
#include "sync.h"
#include "util.h"
#include "base58.h"
#include "addressindex.h"
#include "wallet.h"
#include "init.h"
using namespace std;
using namespace json_spirit;
// Forward declarations from rpcblockchain.cpp
extern Object blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool fPrintTransactionDetail);
extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, json_spirit::Object& entry);
// Forward declaration from trianglesrpc.cpp
extern bool HTTPAuthorized(map<string, string>& mapHeaders);
extern string rfc1123Time();
// ============================================================================
// Rate limiter
// ============================================================================
struct IPRateLimiter
{
int64_t nTokens;
int64_t nLastRefill;
IPRateLimiter() : nTokens(0), nLastRefill(0) {}
bool Allow(int64_t nMaxTokens, int64_t nRefillRate)
{
int64_t nNow = GetTime();
if (nLastRefill == 0) {
nLastRefill = nNow;
nTokens = nMaxTokens;
}
// Refill tokens
int64_t nElapsed = nNow - nLastRefill;
if (nElapsed > 0) {
nTokens += nElapsed * nRefillRate;
if (nTokens > nMaxTokens)
nTokens = nMaxTokens;
nLastRefill = nNow;
}
if (nTokens > 0) {
nTokens--;
return true;
}
return false;
}
};
static CCriticalSection cs_rateLimiter;
static map<string, IPRateLimiter> mapRateLimiters;
static int64_t nLastCleanup = 0;
bool CheckRESTRateLimit(const string& strIP)
{
int64_t nLimit = GetArg("-restratelimit", 30);
int64_t nBurst = nLimit * 2; // burst = 2x sustained rate
if (nLimit <= 0)
return true; // rate limiting disabled
LOCK(cs_rateLimiter);
// Periodic cleanup of stale entries (every 60s)
int64_t nNow = GetTime();
if (nNow - nLastCleanup > 60) {
map<string, IPRateLimiter>::iterator it = mapRateLimiters.begin();
while (it != mapRateLimiters.end()) {
if (nNow - it->second.nLastRefill > 300) // 5 min stale
mapRateLimiters.erase(it++);
else
++it;
}
nLastCleanup = nNow;
}
return mapRateLimiters[strIP].Allow(nBurst, nLimit);
}
// ============================================================================
// HTTP response helpers
// ============================================================================
string HTTPReplyREST(int nStatus, const string& strMsg, const string& contentType)
{
string strCorsOrigin = GetArg(std::string_view{"-restcorsorigin"}, std::string_view{"*"});
const char *cStatus;
if (nStatus == 200) cStatus = "OK";
else if (nStatus == 204) cStatus = "No Content";
else if (nStatus == 400) cStatus = "Bad Request";
else if (nStatus == 401) cStatus = "Unauthorized";
else if (nStatus == 403) cStatus = "Forbidden";
else if (nStatus == 404) cStatus = "Not Found";
else if (nStatus == 429) cStatus = "Too Many Requests";
else if (nStatus == 500) cStatus = "Internal Server Error";
else if (nStatus == 503) cStatus = "Service Unavailable";
else cStatus = "";
return strprintf(
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Content-Length: %" PRIszu "\r\n"
"Content-Type: %s\r\n"
"Access-Control-Allow-Origin: %s\r\n"
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type, Authorization\r\n"
"Access-Control-Max-Age: 86400\r\n"
"Server: Triangles/%s\r\n"
"\r\n"
"%s",
nStatus,
cStatus,
rfc1123Time().c_str(),
strMsg.size(),
contentType.c_str(),
strCorsOrigin.c_str(),
FormatFullVersion().c_str(),
strMsg.c_str());
}
// ============================================================================
// URL parsing
// ============================================================================
static void ParseRESTPath(const string& strURI, vector<string>& parts, map<string, string>& queryParams)
{
string path = strURI;
// Split query string
size_t qpos = path.find('?');
string queryString;
if (qpos != string::npos) {
queryString = path.substr(qpos + 1);
path = path.substr(0, qpos);
}
// Split path into parts
parts = SplitString(path, '/');
// Parse query parameters
if (!queryString.empty()) {
auto pairs = SplitString(queryString, '&');
for (size_t i = 0; i < pairs.size(); i++) {
size_t eq = pairs[i].find('=');
if (eq != string::npos)
queryParams[pairs[i].substr(0, eq)] = pairs[i].substr(eq + 1);
}
}
}
bool IsRESTPath(const string& strURI)
{
return strURI.size() >= 6 && strURI.substr(0, 6) == "/rest/";
}
// ============================================================================
// Auth helpers
// ============================================================================
static bool RESTAuthorized(map<string, string>& mapHeaders)
{
// Check Bearer token first (if -restapikey is set)
string strApiKey = GetArg(std::string_view{"-restapikey"}, std::string_view{""});
if (!strApiKey.empty()) {
string strAuth = mapHeaders.count("authorization") ? mapHeaders["authorization"] : "";
if (strAuth.substr(0, 7) == "Bearer ") {
string strToken = strAuth.substr(7);
strToken = TrimString(strToken);
if (TimingResistantEqual(strToken, strApiKey))
return true;
}
}
// Fall back to Basic Auth (same as RPC)
if (mapHeaders.count("authorization"))
return HTTPAuthorized(mapHeaders);
return false;
}
// ============================================================================
// JSON error helper
// ============================================================================
static string RESTError(const string& message, int code = -1)
{
Object obj;
obj.push_back(Pair("error", message));
if (code != -1)
obj.push_back(Pair("code", code));
return write_string(Value(obj), false) + "\n";
}
// ============================================================================
// Helper: call an RPC method and return JSON string
// ============================================================================
static bool CallRPCMethod(const string& method, const Array& params,
string& strReply, int& nStatus)
{
try {
Value result = tableRPC.execute(method, params);
strReply = write_string(result, false) + "\n";
nStatus = HTTP_OK;
return true;
}
catch (Object& objError) {
int code = find_value(objError, "code").get_int();
string msg = find_value(objError, "message").get_str();
if (code == RPC_METHOD_NOT_FOUND) nStatus = HTTP_NOT_FOUND;
else if (code == RPC_WALLET_UNLOCK_NEEDED) nStatus = HTTP_FORBIDDEN;
else if (code == RPC_INVALID_PARAMETER || code == RPC_INVALID_ADDRESS_OR_KEY) nStatus = HTTP_BAD_REQUEST;
else nStatus = HTTP_INTERNAL_SERVER_ERROR;
strReply = RESTError(msg, code);
return true;
}
catch (std::exception& e) {
nStatus = HTTP_INTERNAL_SERVER_ERROR;
strReply = RESTError(e.what());
return true;
}
}
// ============================================================================
// Public endpoint handlers
// ============================================================================
// GET /rest/chaininfo
static bool HandleChainInfo(string& strReply, int& nStatus)
{
LOCK(cs_main);
Object obj, diff;
obj.push_back(Pair("chain", fTestNet ? string("test") : string("main")));
obj.push_back(Pair("blocks", (int)nBestHeight));
obj.push_back(Pair("bestblockhash", hashBestChain.GetHex()));
diff.push_back(Pair("proof-of-work", GetDifficulty()));
diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true))));
obj.push_back(Pair("difficulty", diff));
obj.push_back(Pair("moneysupply", ValueFromAmount(pindexBest->nMoneySupply)));
strReply = write_string(Value(obj), false) + "\n";
nStatus = HTTP_OK;
return true;
}
// GET /rest/block/{hash_or_param}[.hex]
static bool HandleBlock(const string& param, const string& format, string& strReply, int& nStatus)
{
LOCK(cs_main);
uint256 hash(param);
if (mapBlockIndex.count(hash) == 0) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Block not found");
return true;
}
CBlock block;
CBlockIndex* pblockindex = mapBlockIndex[hash];
block.ReadFromDisk(pblockindex, true);
if (format == "hex") {
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
ssBlock << block;
strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n";
} else {
Object obj = blockToJSON(block, pblockindex, false);
strReply = write_string(Value(obj), false) + "\n";
}
nStatus = HTTP_OK;
return true;
}
// GET /rest/blockheader/{hash}
static bool HandleBlockHeader(const string& param, string& strReply, int& nStatus)
{
LOCK(cs_main);
uint256 hash(param);
if (mapBlockIndex.count(hash) == 0) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Block not found");
return true;
}
CBlockIndex* pblockindex = mapBlockIndex[hash];
Object result;
result.push_back(Pair("hash", pblockindex->GetBlockHash().GetHex()));
result.push_back(Pair("confirmations", pindexBest->nHeight - pblockindex->nHeight + 1));
result.push_back(Pair("height", pblockindex->nHeight));
result.push_back(Pair("version", pblockindex->nVersion));
result.push_back(Pair("merkleroot", pblockindex->hashMerkleRoot.GetHex()));
result.push_back(Pair("time", (int64_t)pblockindex->GetBlockTime()));
result.push_back(Pair("nonce", (uint64_t)pblockindex->nNonce));
result.push_back(Pair("bits", HexBits(pblockindex->nBits)));
result.push_back(Pair("difficulty", GetDifficulty(pblockindex)));
result.push_back(Pair("flags", strprintf("%s%s",
pblockindex->IsProofOfStake() ? "proof-of-stake" : "proof-of-work",
pblockindex->GeneratedStakeModifier() ? " stake-modifier" : "")));
if (pblockindex->pprev)
result.push_back(Pair("previousblockhash", pblockindex->pprev->GetBlockHash().GetHex()));
if (pblockindex->pnext)
result.push_back(Pair("nextblockhash", pblockindex->pnext->GetBlockHash().GetHex()));
strReply = write_string(Value(result), false) + "\n";
nStatus = HTTP_OK;
return true;
}
// GET /rest/tx/{txid}[.hex]
static bool HandleTx(const string& param, const string& format, string& strReply, int& nStatus)
{
LOCK(cs_main);
uint256 hash(param);
CTransaction tx;
uint256 hashBlock = 0;
if (!GetTransaction(hash, tx, hashBlock)) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Transaction not found");
return true;
}
if (format == "hex") {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << tx;
strReply = HexStr(ssTx.begin(), ssTx.end()) + "\n";
} else {
Object obj;
obj.push_back(Pair("txid", tx.GetHash().GetHex()));
TxToJSON(tx, hashBlock, obj);
strReply = write_string(Value(obj), false) + "\n";
}
nStatus = HTTP_OK;
return true;
}
// GET /rest/blockhashbyheight/{n}
static bool HandleBlockHashByHeight(const string& param, string& strReply, int& nStatus)
{
LOCK(cs_main);
int nHeight = atoi(param.c_str());
if (nHeight < 0 || nHeight > nBestHeight) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Block height out of range");
return true;
}
CBlockIndex* pblockindex = FindBlockByHeight(nHeight);
Object obj;
obj.push_back(Pair("blockhash", pblockindex->phashBlock->GetHex()));
strReply = write_string(Value(obj), false) + "\n";
nStatus = HTTP_OK;
return true;
}
// GET /rest/blockbyheight/{n}
static bool HandleBlockByHeight(const string& param, const string& format, string& strReply, int& nStatus)
{
LOCK(cs_main);
int nHeight = atoi(param.c_str());
if (nHeight < 0 || nHeight > nBestHeight) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Block height out of range");
return true;
}
CBlockIndex* pblockindex = FindBlockByHeight(nHeight);
CBlock block;
block.ReadFromDisk(pblockindex, true);
if (format == "hex") {
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
ssBlock << block;
strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n";
} else {
Object obj = blockToJSON(block, pblockindex, false);
strReply = write_string(Value(obj), false) + "\n";
}
nStatus = HTTP_OK;
return true;
}
// GET /rest/mempool
static bool HandleMempool(string& strReply, int& nStatus)
{
vector<uint256> vtxid;
mempool.queryHashes(vtxid);
Array a;
for (const uint256& hash : vtxid)
a.push_back(hash.ToString());
strReply = write_string(Value(a), false) + "\n";
nStatus = HTTP_OK;
return true;
}
// GET /rest/difficulty
static bool HandleDifficulty(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getdifficulty", params, strReply, nStatus);
}
// GET /rest/supply
static bool HandleSupply(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("gettxoutsetinfo", params, strReply, nStatus);
}
// GET /rest/staking
static bool HandleStaking(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getstakinginfo", params, strReply, nStatus);
}
// GET /rest/mining
static bool HandleMining(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getmininginfo", params, strReply, nStatus);
}
// GET /rest/subsidy
static bool HandleSubsidy(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getsubsidy", params, strReply, nStatus);
}
// GET /rest/estimatefee
static bool HandleEstimateFee(string& strReply, int& nStatus)
{
Array params;
params.push_back(6); // default 6 blocks
return CallRPCMethod("estimatefee", params, strReply, nStatus);
}
// GET /rest/checkpoint
static bool HandleCheckpoint(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getcheckpoint", params, strReply, nStatus);
}
// GET /rest/network
static bool HandleNetwork(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getnetworkinfo", params, strReply, nStatus);
}
// GET /rest/peers
static bool HandlePeers(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getpeerinfo", params, strReply, nStatus);
}
// GET /rest/validate/{address}
static bool HandleValidate(const string& addr, string& strReply, int& nStatus)
{
Array params;
params.push_back(addr);
return CallRPCMethod("validateaddress", params, strReply, nStatus);
}
// GET /rest/address/{addr}/balance
static bool HandleAddressBalance(const string& addr, string& strReply, int& nStatus)
{
if (!fAddressIndex) {
nStatus = 503;
strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1");
return true;
}
Object addrObj;
Array addrArray;
addrArray.push_back(addr);
addrObj.push_back(Pair("addresses", addrArray));
Array params;
params.push_back(addrObj);
return CallRPCMethod("getaddressbalance", params, strReply, nStatus);
}
// GET /rest/address/{addr}/utxos
static bool HandleAddressUtxos(const string& addr, string& strReply, int& nStatus)
{
if (!fAddressIndex) {
nStatus = 503;
strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1");
return true;
}
Object addrObj;
Array addrArray;
addrArray.push_back(addr);
addrObj.push_back(Pair("addresses", addrArray));
Array params;
params.push_back(addrObj);
return CallRPCMethod("getaddressutxos", params, strReply, nStatus);
}
// GET /rest/address/{addr}/txids[?start=N&end=N]
static bool HandleAddressTxids(const string& addr, const map<string, string>& queryParams,
string& strReply, int& nStatus)
{
if (!fAddressIndex) {
nStatus = 503;
strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1");
return true;
}
Object addrObj;
Array addrArray;
addrArray.push_back(addr);
addrObj.push_back(Pair("addresses", addrArray));
map<string, string>::const_iterator itStart = queryParams.find("start");
map<string, string>::const_iterator itEnd = queryParams.find("end");
if (itStart != queryParams.end())
addrObj.push_back(Pair("start", atoi(itStart->second.c_str())));
if (itEnd != queryParams.end())
addrObj.push_back(Pair("end", atoi(itEnd->second.c_str())));
Array params;
params.push_back(addrObj);
return CallRPCMethod("getaddresstxids", params, strReply, nStatus);
}
// POST /rest/tx/decode body: {"hex":"..."}
static bool HandleTxDecode(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"hex\":\"...\"}");
return true;
}
Object bodyObj = valBody.get_obj();
Value hexVal = find_value(bodyObj, "hex");
if (hexVal.type() != str_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'hex' field in request body");
return true;
}
Array params;
params.push_back(hexVal.get_str());
return CallRPCMethod("decoderawtransaction", params, strReply, nStatus);
}
// POST /rest/tx/send body: {"hex":"..."}
static bool HandleTxSend(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"hex\":\"...\"}");
return true;
}
Object bodyObj = valBody.get_obj();
Value hexVal = find_value(bodyObj, "hex");
if (hexVal.type() != str_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'hex' field in request body");
return true;
}
Array params;
params.push_back(hexVal.get_str());
return CallRPCMethod("sendrawtransaction", params, strReply, nStatus);
}
// ============================================================================
// Wallet endpoint handlers (authenticated)
// ============================================================================
// GET /rest/wallet/info
static bool HandleWalletInfo(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getwalletinfo", params, strReply, nStatus);
}
// GET /rest/wallet/balance
static bool HandleWalletBalance(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getbalance", params, strReply, nStatus);
}
// GET /rest/wallet/transactions[?count=N&skip=N]
static bool HandleWalletTransactions(const map<string, string>& queryParams,
string& strReply, int& nStatus)
{
Array params;
params.push_back("*"); // all accounts
map<string, string>::const_iterator itCount = queryParams.find("count");
map<string, string>::const_iterator itSkip = queryParams.find("skip");
int nCount = 10;
int nSkip = 0;
if (itCount != queryParams.end())
nCount = atoi(itCount->second.c_str());
if (itSkip != queryParams.end())
nSkip = atoi(itSkip->second.c_str());
params.push_back(nCount);
params.push_back(nSkip);
return CallRPCMethod("listtransactions", params, strReply, nStatus);
}
// GET /rest/wallet/transaction/{txid}
static bool HandleWalletTransaction(const string& txid, string& strReply, int& nStatus)
{
Array params;
params.push_back(txid);
return CallRPCMethod("gettransaction", params, strReply, nStatus);
}
// GET /rest/wallet/unspent[?minconf=N&maxconf=N]
static bool HandleWalletUnspent(const map<string, string>& queryParams,
string& strReply, int& nStatus)
{
Array params;
map<string, string>::const_iterator itMin = queryParams.find("minconf");
map<string, string>::const_iterator itMax = queryParams.find("maxconf");
params.push_back(itMin != queryParams.end() ? atoi(itMin->second.c_str()) : 1);
params.push_back(itMax != queryParams.end() ? atoi(itMax->second.c_str()) : 9999999);
return CallRPCMethod("listunspent", params, strReply, nStatus);
}
// GET /rest/wallet/addresses
static bool HandleWalletAddresses(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("listaddressgroupings", params, strReply, nStatus);
}
// GET /rest/wallet/staking
static bool HandleWalletStaking(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getstakinginfo", params, strReply, nStatus);
}
// POST /rest/wallet/address/new body: {} or {"account":"..."}
static bool HandleWalletNewAddress(const string& strBody, string& strReply, int& nStatus)
{
Array params;
if (!strBody.empty()) {
Value valBody;
if (read_string(strBody, valBody) && valBody.type() == obj_type) {
Value acctVal = find_value(valBody.get_obj(), "account");
if (acctVal.type() == str_type)
params.push_back(acctVal.get_str());
}
}
return CallRPCMethod("getnewaddress", params, strReply, nStatus);
}
// POST /rest/wallet/send body: {"address":"...", "amount":N}
static bool HandleWalletSend(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"address\":\"...\", \"amount\":N}");
return true;
}
Object bodyObj = valBody.get_obj();
Value addrVal = find_value(bodyObj, "address");
Value amtVal = find_value(bodyObj, "amount");
if (addrVal.type() != str_type || (amtVal.type() != real_type && amtVal.type() != int_type)) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'address' (string) or 'amount' (number) in request body");
return true;
}
Array params;
params.push_back(addrVal.get_str());
params.push_back(amtVal);
// Optional comment fields
Value commentVal = find_value(bodyObj, "comment");
Value commentToVal = find_value(bodyObj, "comment_to");
if (commentVal.type() == str_type)
params.push_back(commentVal.get_str());
else
params.push_back("");
if (commentToVal.type() == str_type)
params.push_back(commentToVal.get_str());
return CallRPCMethod("sendtoaddress", params, strReply, nStatus);
}
// POST /rest/wallet/sendmany body: {"recipients":{"addr":amount,...}}
static bool HandleWalletSendMany(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"recipients\":{\"addr\":amount,...}}");
return true;
}
Object bodyObj = valBody.get_obj();
Value recipVal = find_value(bodyObj, "recipients");
if (recipVal.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'recipients' object in request body");
return true;
}
Array params;
params.push_back(""); // fromaccount (default)
params.push_back(recipVal); // {addr: amount, ...}
return CallRPCMethod("sendmany", params, strReply, nStatus);
}
// POST /rest/wallet/unlock body: {"passphrase":"...", "timeout":N, "staking_only":bool}
static bool HandleWalletUnlock(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"passphrase\":\"...\", \"timeout\":N}");
return true;
}
Object bodyObj = valBody.get_obj();
Value passVal = find_value(bodyObj, "passphrase");
Value timeVal = find_value(bodyObj, "timeout");
if (passVal.type() != str_type || timeVal.type() != int_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'passphrase' (string) or 'timeout' (integer) in request body");
return true;
}
Array params;
params.push_back(passVal.get_str());
params.push_back(timeVal.get_int());
Value stakingVal = find_value(bodyObj, "staking_only");
if (stakingVal.type() == bool_type)
params.push_back(stakingVal.get_bool());
return CallRPCMethod("walletpassphrase", params, strReply, nStatus);
}
// POST /rest/wallet/lock
static bool HandleWalletLock(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("walletlock", params, strReply, nStatus);
}
// ============================================================================
// Main router
// ============================================================================
bool HandleRESTRequest(const string& strMethod,
const string& strURI,
const string& strBody,
map<string, string>& mapHeaders,
string& strReply,
string& strContentType,
int& nStatus)
{
strContentType = "application/json";
// OPTIONS: CORS preflight
if (strMethod == "OPTIONS") {
nStatus = 204;
strReply = "";
return true;
}
// Parse path
vector<string> parts;
map<string, string> queryParams;
ParseRESTPath(strURI, parts, queryParams);
// parts: ["", "rest", "resource", "param", ...]
if (parts.size() < 3) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Not found");
return true;
}
string resource = parts[2];
// Detect format suffix (.hex, .json)
string format = "json";
string lastPart = parts.size() > 3 ? parts[parts.size() - 1] : "";
size_t dotPos = lastPart.rfind('.');
string param;
if (dotPos != string::npos) {
param = lastPart.substr(0, dotPos);
format = lastPart.substr(dotPos + 1);
} else {
param = lastPart;
}
if (format == "hex")
strContentType = "text/plain";
// ---- Wallet endpoints (authenticated) ----
if (resource == "wallet") {
if (!RESTAuthorized(mapHeaders)) {
nStatus = HTTP_UNAUTHORIZED;
strReply = RESTError("Authentication required for wallet endpoints");
return true;
}
if (parts.size() < 4) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Unknown wallet endpoint");
return true;
}
string walletResource = parts[3];
if (strMethod == "GET") {
if (walletResource == "info")
return HandleWalletInfo(strReply, nStatus);
if (walletResource == "balance")
return HandleWalletBalance(strReply, nStatus);
if (walletResource == "transactions")
return HandleWalletTransactions(queryParams, strReply, nStatus);
if (walletResource == "transaction" && parts.size() > 4)
return HandleWalletTransaction(parts[4], strReply, nStatus);
if (walletResource == "unspent")
return HandleWalletUnspent(queryParams, strReply, nStatus);
if (walletResource == "addresses")
return HandleWalletAddresses(strReply, nStatus);
if (walletResource == "staking")
return HandleWalletStaking(strReply, nStatus);
}
else if (strMethod == "POST") {
if (walletResource == "address" && parts.size() > 4 && parts[4] == "new")
return HandleWalletNewAddress(strBody, strReply, nStatus);
if (walletResource == "send")
return HandleWalletSend(strBody, strReply, nStatus);
if (walletResource == "sendmany")
return HandleWalletSendMany(strBody, strReply, nStatus);
if (walletResource == "unlock")
return HandleWalletUnlock(strBody, strReply, nStatus);
if (walletResource == "lock")
return HandleWalletLock(strReply, nStatus);
}
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Unknown wallet endpoint");
return true;
}
// ---- Public GET endpoints ----
if (strMethod == "GET") {
if (resource == "chaininfo")
return HandleChainInfo(strReply, nStatus);
if (resource == "block" && !param.empty())
return HandleBlock(param, format, strReply, nStatus);
if (resource == "blockheader" && !param.empty())
return HandleBlockHeader(param, strReply, nStatus);
if (resource == "tx") {
// /rest/tx/decode and /rest/tx/send are POST-only
if (!param.empty())
return HandleTx(param, format, strReply, nStatus);
}
if (resource == "blockhashbyheight" && !param.empty())
return HandleBlockHashByHeight(param, strReply, nStatus);
if (resource == "blockbyheight" && !param.empty())
return HandleBlockByHeight(param, format, strReply, nStatus);
if (resource == "mempool")
return HandleMempool(strReply, nStatus);
if (resource == "difficulty")
return HandleDifficulty(strReply, nStatus);
if (resource == "supply")
return HandleSupply(strReply, nStatus);
if (resource == "staking")
return HandleStaking(strReply, nStatus);
if (resource == "mining")
return HandleMining(strReply, nStatus);
if (resource == "subsidy")
return HandleSubsidy(strReply, nStatus);
if (resource == "estimatefee")
return HandleEstimateFee(strReply, nStatus);
if (resource == "checkpoint")
return HandleCheckpoint(strReply, nStatus);
if (resource == "network")
return HandleNetwork(strReply, nStatus);
if (resource == "peers")
return HandlePeers(strReply, nStatus);
if (resource == "validate" && !param.empty())
return HandleValidate(param, strReply, nStatus);
// Address endpoints: /rest/address/{addr}/balance etc.
if (resource == "address" && parts.size() >= 5) {
string addr = parts[3];
string addrAction = parts[4];
if (addrAction == "balance")
return HandleAddressBalance(addr, strReply, nStatus);
if (addrAction == "utxos")
return HandleAddressUtxos(addr, strReply, nStatus);
if (addrAction == "txids")
return HandleAddressTxids(addr, queryParams, strReply, nStatus);
}
}
// ---- Public POST endpoints ----
if (strMethod == "POST") {
if (resource == "tx" && parts.size() >= 4) {
string txAction = parts[3];
if (txAction == "decode")
return HandleTxDecode(strBody, strReply, nStatus);
if (txAction == "send")
return HandleTxSend(strBody, strReply, nStatus);
}
}
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Unknown REST endpoint");
return true;
}