rest: add endpoint for estimatesmartfee

* write REST interface for getting estimated fee
* update docs about REST interface for fee estimation
* add test
This commit is contained in:
Luke Dashjr 2023-07-29 20:14:28 +00:00
parent 1248d0da22
commit bada19a8e8
3 changed files with 94 additions and 0 deletions

View File

@ -144,6 +144,12 @@ Refer to the `getrawmempool` RPC help for details. Defaults to setting
*Query parameters for `verbose` and `mempool_sequence` available in 25.0 and up.* *Query parameters for `verbose` and `mempool_sequence` available in 25.0 and up.*
#### Fees
`GET /rest/fee/<MODE>/<TARGET>.json`
Returns fee and blocknumber where estimation was found. `<MODE>` should be one of `<unset|conservative|economical>`.
`<TARGET>` is the desired confirmation time (in block height).
Risks Risks
------------- -------------
Running a web browser on the same node with a REST enabled bitcoind can be a risk. Accessing prepared XSS websites could read out tx/block data of your node by placing links like `<script src="http://127.0.0.1:8332/rest/tx/1234567890.json">` which might break the nodes privacy. Running a web browser on the same node with a REST enabled bitcoind can be a risk. Accessing prepared XSS websites could read out tx/block data of your node by placing links like `<script src="http://127.0.0.1:8332/rest/tx/1234567890.json">` which might break the nodes privacy.

View File

@ -10,6 +10,7 @@
#include <blockfilter.h> #include <blockfilter.h>
#include <chain.h> #include <chain.h>
#include <chainparams.h> #include <chainparams.h>
#include <common/messages.h>
#include <core_io.h> #include <core_io.h>
#include <flatfile.h> #include <flatfile.h>
#include <httpserver.h> #include <httpserver.h>
@ -31,6 +32,7 @@
#include <util/check.h> #include <util/check.h>
#include <util/strencodings.h> #include <util/strencodings.h>
#include <validation.h> #include <validation.h>
#include <policy/fees.h>
#include <any> #include <any>
#include <vector> #include <vector>
@ -1003,6 +1005,70 @@ static bool rest_blockhash_by_height(const std::any& context, HTTPRequest* req,
} }
} }
static bool rest_getfee(const std::any& context, HTTPRequest* req, const std::string& strURIPart) {
if (!CheckWarmup(req)) {
return false;
}
const NodeContext* const node = GetNodeContext(context, req);
if (!node) return false;
const CBlockPolicyEstimator* const fee_estimator = node->fee_estimator.get();
if (!fee_estimator) return false;
std::string param;
const RESTResponseFormat rf = ParseDataFormat(param, strURIPart);
switch (rf) {
case RESTResponseFormat::JSON: {
std::vector<std::string> path = SplitString(param, '/');
path.erase(path.begin());
// check url scheme is correct
if (path.size() != 2) {
return RESTERR(req, HTTP_BAD_REQUEST, "Path must be /rest/fee/<MODE>/<TARGET>.json");
}
// check estimation mode is valid
const auto modestr = ToUpper(path[0]);
FeeEstimateMode mode;
if (!common::FeeModeFromString(modestr, mode)){
return RESTERR(req, HTTP_BAD_REQUEST, "<MODE> must be one of <unset|economical|conservative>");
}
// type conversions for estimateSmartFee
bool conservative = mode == FeeEstimateMode::CONSERVATIVE;
const auto parsed_conf_target{ToIntegral<int64_t>(path[1])};
if (!parsed_conf_target.has_value()) {
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Unable to parse confirmation target to int"));
}
int64_t conf_target{*parsed_conf_target};
unsigned int max_target = fee_estimator->HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
if (conf_target < 1 || (unsigned int)conf_target > max_target) {
return RESTERR(req, HTTP_BAD_REQUEST, strprintf("Invalid confirmation target, must be in between %u - %u", 1, max_target));
}
// perform fee estimation
FeeCalculation feeCalc;
CFeeRate estimatedfee = fee_estimator->estimateSmartFee(conf_target, &feeCalc, conservative);
// create json for replying
UniValue feejson(UniValue::VOBJ);
if (estimatedfee != CFeeRate(0)) {
feejson.pushKV("feerate", ValueFromAmount(estimatedfee.GetFeePerK()));
} else {
return RESTERR(req, HTTP_SERVICE_UNAVAILABLE, "Insufficient data or no feerate found");
}
feejson.pushKV("blocks", feeCalc.returnedTarget);
// reply
std::string strJSON = feejson.write() + "\n";
req->WriteHeader("Content-Type", "application/json");
req->WriteReply(HTTP_OK, strJSON);
return true;
}
default: {
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: json)");
}
}
}
static const struct { static const struct {
const char* prefix; const char* prefix;
bool (*handler)(const std::any& context, HTTPRequest* req, const std::string& strReq); bool (*handler)(const std::any& context, HTTPRequest* req, const std::string& strReq);
@ -1019,6 +1085,7 @@ static const struct {
{"/rest/deploymentinfo/", rest_deploymentinfo}, {"/rest/deploymentinfo/", rest_deploymentinfo},
{"/rest/deploymentinfo", rest_deploymentinfo}, {"/rest/deploymentinfo", rest_deploymentinfo},
{"/rest/blockhashbyheight/", rest_blockhash_by_height}, {"/rest/blockhashbyheight/", rest_blockhash_by_height},
{"/rest/fee", rest_getfee},
}; };
void StartREST(const std::any& context) void StartREST(const std::any& context)

View File

@ -49,6 +49,9 @@ def filter_output_indices_by_value(vouts, value):
yield vout['n'] yield vout['n']
class RESTTest (BitcoinTestFramework): class RESTTest (BitcoinTestFramework):
def add_options(self, parser):
self.add_wallet_options(parser)
def set_test_params(self): def set_test_params(self):
self.num_nodes = 2 self.num_nodes = 2
self.extra_args = [["-rest", "-blockfilterindex=1"], []] self.extra_args = [["-rest", "-blockfilterindex=1"], []]
@ -440,5 +443,23 @@ class RESTTest (BitcoinTestFramework):
resp = self.test_rest_request(f"/deploymentinfo/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400) resp = self.test_rest_request(f"/deploymentinfo/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400)
assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {INVALID_PARAM}") assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {INVALID_PARAM}")
if self.is_wallet_compiled():
self.import_deterministic_coinbase_privkeys()
# Random address so node1's balance doesn't increase
not_related_address = "2MxqoHEdNQTyYeX1mHcbrrpzgojbosTpCvJ"
# Prepare for Fee estimation
for i in range(18):
self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.1)
self.sync_all()
self.generatetoaddress(self.nodes[1], 1, not_related_address)
self.sync_all()
json_obj = self.test_rest_request("/fee/conservative/1")
assert_greater_than(float(json_obj["feerate"]), 0)
assert_greater_than(int(json_obj["blocks"]), 0)
if __name__ == '__main__': if __name__ == '__main__':
RESTTest(__file__).main() RESTTest(__file__).main()