aboutsummaryrefslogtreecommitdiff
path: root/src/rpc/fees.cpp
blob: aa047bdea879463337a5b0ee819a971fca4a39db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Copyright (c) 2010 Satoshi Nakamoto
// Copyright (c) 2009-2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <core_io.h>
#include <policy/feerate.h>
#include <policy/fees.h>
#include <rpc/protocol.h>
#include <rpc/request.h>
#include <rpc/server.h>
#include <rpc/server_util.h>
#include <rpc/util.h>
#include <txmempool.h>
#include <univalue.h>
#include <util/fees.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <string>

namespace node {
struct NodeContext;
}

using node::NodeContext;

static RPCHelpMan estimatesmartfee()
{
    return RPCHelpMan{"estimatesmartfee",
        "\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
        "confirmation within conf_target blocks if possible and return the number of blocks\n"
        "for which the estimate is valid. Uses virtual transaction size as defined\n"
        "in BIP 141 (witness data is discounted).\n",
        {
            {"conf_target", RPCArg::Type::NUM, RPCArg::Optional::NO, "Confirmation target in blocks (1 - 1008)"},
            {"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"conservative"}, "The fee estimate mode.\n"
            "Whether to return a more conservative estimate which also satisfies\n"
            "a longer history. A conservative estimate potentially returns a\n"
            "higher feerate and is more likely to be sufficient for the desired\n"
            "target, but is not as responsive to short term drops in the\n"
            "prevailing fee market. Must be one of (case insensitive):\n"
             "\"" + FeeModes("\"\n\"") + "\""},
        },
        RPCResult{
            RPCResult::Type::OBJ, "", "",
            {
                {RPCResult::Type::NUM, "feerate", /*optional=*/true, "estimate fee rate in " + CURRENCY_UNIT + "/kvB (only present if no errors were encountered)"},
                {RPCResult::Type::ARR, "errors", /*optional=*/true, "Errors encountered during processing (if there are any)",
                    {
                        {RPCResult::Type::STR, "", "error"},
                    }},
                {RPCResult::Type::NUM, "blocks", "block number where estimate was found\n"
                "The request target will be clamped between 2 and the highest target\n"
                "fee estimation is able to return based on how long it has been running.\n"
                "An error is returned if not enough transactions and blocks\n"
                "have been observed to make an estimate for any number of blocks."},
        }},
        RPCExamples{
            HelpExampleCli("estimatesmartfee", "6") +
            HelpExampleRpc("estimatesmartfee", "6")
        },
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
        {
            RPCTypeCheck(request.params, {UniValue::VNUM, UniValue::VSTR});
            RPCTypeCheckArgument(request.params[0], UniValue::VNUM);

            CBlockPolicyEstimator& fee_estimator = EnsureAnyFeeEstimator(request.context);
            const NodeContext& node = EnsureAnyNodeContext(request.context);
            const CTxMemPool& mempool = EnsureMemPool(node);

            unsigned int max_target = fee_estimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
            unsigned int conf_target = ParseConfirmTarget(request.params[0], max_target);
            bool conservative = true;
            if (!request.params[1].isNull()) {
                FeeEstimateMode fee_mode;
                if (!FeeModeFromString(request.params[1].get_str(), fee_mode)) {
                    throw JSONRPCError(RPC_INVALID_PARAMETER, InvalidEstimateModeErrorMessage());
                }
                if (fee_mode == FeeEstimateMode::ECONOMICAL) conservative = false;
            }

            UniValue result(UniValue::VOBJ);
            UniValue errors(UniValue::VARR);
            FeeCalculation feeCalc;
            CFeeRate feeRate{fee_estimator.estimateSmartFee(conf_target, &feeCalc, conservative)};
            if (feeRate != CFeeRate(0)) {
                CFeeRate min_mempool_feerate{mempool.GetMinFee()};
                CFeeRate min_relay_feerate{mempool.m_min_relay_feerate};
                feeRate = std::max({feeRate, min_mempool_feerate, min_relay_feerate});
                result.pushKV("feerate", ValueFromAmount(feeRate.GetFeePerK()));
            } else {
                errors.push_back("Insufficient data or no feerate found");
                result.pushKV("errors", errors);
            }
            result.pushKV("blocks", feeCalc.returnedTarget);
            return result;
        },
    };
}

static RPCHelpMan estimaterawfee()
{
    return RPCHelpMan{"estimaterawfee",
        "\nWARNING: This interface is unstable and may disappear or change!\n"
        "\nWARNING: This is an advanced API call that is tightly coupled to the specific\n"
        "implementation of fee estimation. The parameters it can be called with\n"
        "and the results it returns will change if the internal implementation changes.\n"
        "\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
        "confirmation within conf_target blocks if possible. Uses virtual transaction size as\n"
        "defined in BIP 141 (witness data is discounted).\n",
        {
            {"conf_target", RPCArg::Type::NUM, RPCArg::Optional::NO, "Confirmation target in blocks (1 - 1008)"},
            {"threshold", RPCArg::Type::NUM, RPCArg::Default{0.95}, "The proportion of transactions in a given feerate range that must have been\n"
            "confirmed within conf_target in order to consider those feerates as high enough and proceed to check\n"
            "lower buckets."},
        },
        RPCResult{
            RPCResult::Type::OBJ, "", "Results are returned for any horizon which tracks blocks up to the confirmation target",
            {
                {RPCResult::Type::OBJ, "short", /*optional=*/true, "estimate for short time horizon",
                    {
                        {RPCResult::Type::NUM, "feerate", /*optional=*/true, "estimate fee rate in " + CURRENCY_UNIT + "/kvB"},
                        {RPCResult::Type::NUM, "decay", "exponential decay (per block) for historical moving average of confirmation data"},
                        {RPCResult::Type::NUM, "scale", "The resolution of confirmation targets at this time horizon"},
                        {RPCResult::Type::OBJ, "pass", /*optional=*/true, "information about the lowest range of feerates to succeed in meeting the threshold",
                        {
                                {RPCResult::Type::NUM, "startrange", "start of feerate range"},
                                {RPCResult::Type::NUM, "endrange", "end of feerate range"},
                                {RPCResult::Type::NUM, "withintarget", "number of txs over history horizon in the feerate range that were confirmed within target"},
                                {RPCResult::Type::NUM, "totalconfirmed", "number of txs over history horizon in the feerate range that were confirmed at any point"},
                                {RPCResult::Type::NUM, "inmempool", "current number of txs in mempool in the feerate range unconfirmed for at least target blocks"},
                                {RPCResult::Type::NUM, "leftmempool", "number of txs over history horizon in the feerate range that left mempool unconfirmed after target"},
                        }},
                        {RPCResult::Type::OBJ, "fail", /*optional=*/true, "information about the highest range of feerates to fail to meet the threshold",
                        {
                            {RPCResult::Type::ELISION, "", ""},
                        }},
                        {RPCResult::Type::ARR, "errors", /*optional=*/true, "Errors encountered during processing (if there are any)",
                        {
                            {RPCResult::Type::STR, "error", ""},
                        }},
                }},
                {RPCResult::Type::OBJ, "medium", /*optional=*/true, "estimate for medium time horizon",
                {
                    {RPCResult::Type::ELISION, "", ""},
                }},
                {RPCResult::Type::OBJ, "long", /*optional=*/true, "estimate for long time horizon",
                {
                    {RPCResult::Type::ELISION, "", ""},
                }},
            }},
        RPCExamples{
            HelpExampleCli("estimaterawfee", "6 0.9")
        },
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
        {
            RPCTypeCheck(request.params, {UniValue::VNUM, UniValue::VNUM}, true);
            RPCTypeCheckArgument(request.params[0], UniValue::VNUM);

            CBlockPolicyEstimator& fee_estimator = EnsureAnyFeeEstimator(request.context);

            unsigned int max_target = fee_estimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
            unsigned int conf_target = ParseConfirmTarget(request.params[0], max_target);
            double threshold = 0.95;
            if (!request.params[1].isNull()) {
                threshold = request.params[1].get_real();
            }
            if (threshold < 0 || threshold > 1) {
                throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid threshold");
            }

            UniValue result(UniValue::VOBJ);

            for (const FeeEstimateHorizon horizon : ALL_FEE_ESTIMATE_HORIZONS) {
                CFeeRate feeRate;
                EstimationResult buckets;

                // Only output results for horizons which track the target
                if (conf_target > fee_estimator.HighestTargetTracked(horizon)) continue;

                feeRate = fee_estimator.estimateRawFee(conf_target, threshold, horizon, &buckets);
                UniValue horizon_result(UniValue::VOBJ);
                UniValue errors(UniValue::VARR);
                UniValue passbucket(UniValue::VOBJ);
                passbucket.pushKV("startrange", round(buckets.pass.start));
                passbucket.pushKV("endrange", round(buckets.pass.end));
                passbucket.pushKV("withintarget", round(buckets.pass.withinTarget * 100.0) / 100.0);
                passbucket.pushKV("totalconfirmed", round(buckets.pass.totalConfirmed * 100.0) / 100.0);
                passbucket.pushKV("inmempool", round(buckets.pass.inMempool * 100.0) / 100.0);
                passbucket.pushKV("leftmempool", round(buckets.pass.leftMempool * 100.0) / 100.0);
                UniValue failbucket(UniValue::VOBJ);
                failbucket.pushKV("startrange", round(buckets.fail.start));
                failbucket.pushKV("endrange", round(buckets.fail.end));
                failbucket.pushKV("withintarget", round(buckets.fail.withinTarget * 100.0) / 100.0);
                failbucket.pushKV("totalconfirmed", round(buckets.fail.totalConfirmed * 100.0) / 100.0);
                failbucket.pushKV("inmempool", round(buckets.fail.inMempool * 100.0) / 100.0);
                failbucket.pushKV("leftmempool", round(buckets.fail.leftMempool * 100.0) / 100.0);

                // CFeeRate(0) is used to indicate error as a return value from estimateRawFee
                if (feeRate != CFeeRate(0)) {
                    horizon_result.pushKV("feerate", ValueFromAmount(feeRate.GetFeePerK()));
                    horizon_result.pushKV("decay", buckets.decay);
                    horizon_result.pushKV("scale", (int)buckets.scale);
                    horizon_result.pushKV("pass", passbucket);
                    // buckets.fail.start == -1 indicates that all buckets passed, there is no fail bucket to output
                    if (buckets.fail.start != -1) horizon_result.pushKV("fail", failbucket);
                } else {
                    // Output only information that is still meaningful in the event of error
                    horizon_result.pushKV("decay", buckets.decay);
                    horizon_result.pushKV("scale", (int)buckets.scale);
                    horizon_result.pushKV("fail", failbucket);
                    errors.push_back("Insufficient data or no feerate found which meets threshold");
                    horizon_result.pushKV("errors", errors);
                }
                result.pushKV(StringForFeeEstimateHorizon(horizon), horizon_result);
            }
            return result;
        },
    };
}

void RegisterFeeRPCCommands(CRPCTable& t)
{
    static const CRPCCommand commands[]{
        {"util", &estimatesmartfee},
        {"hidden", &estimaterawfee},
    };
    for (const auto& c : commands) {
        t.appendCommand(c.name, &c);
    }
}