Coverage Report

Created: 2026-07-14 18:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/bitcoin/src/rpc/fees.cpp
Line
Count
Source
1
// Copyright (c) 2010 Satoshi Nakamoto
2
// Copyright (c) 2009-present The Bitcoin Core developers
3
// Distributed under the MIT software license, see the accompanying
4
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
6
#include <common/messages.h>
7
#include <core_io.h>
8
#include <node/context.h>
9
#include <policy/feerate.h>
10
#include <policy/fees/block_policy_estimator.h>
11
#include <rpc/protocol.h>
12
#include <rpc/request.h>
13
#include <rpc/server.h>
14
#include <rpc/server_util.h>
15
#include <rpc/util.h>
16
#include <txmempool.h>
17
#include <univalue.h>
18
#include <util/fees.h>
19
#include <validationinterface.h>
20
21
#include <algorithm>
22
#include <array>
23
#include <cmath>
24
#include <string>
25
#include <string_view>
26
27
using common::FeeModeFromString;
28
using common::FeeModesDetail;
29
using common::InvalidEstimateModeErrorMessage;
30
using node::NodeContext;
31
32
static RPCMethod estimatesmartfee()
33
54
{
34
54
    return RPCMethod{
35
54
        "estimatesmartfee",
36
54
        "Estimates the approximate fee per kilobyte needed for a transaction to begin\n"
37
54
        "confirmation within conf_target blocks if possible and return the number of blocks\n"
38
54
        "for which the estimate is valid. Uses virtual transaction size as defined\n"
39
54
        "in BIP 141 (witness data is discounted).\n",
40
54
        {
41
54
            {"conf_target", RPCArg::Type::NUM, RPCArg::Optional::NO, "Confirmation target in blocks (1 - 1008)"},
42
54
            {"estimate_mode", RPCArg::Type::STR, RPCArg::Default{"economical"}, "The fee estimate mode.\n"
43
54
              + FeeModesDetail(std::string("default mode will be used"))},
44
54
        },
45
54
        RPCResult{
46
54
            RPCResult::Type::OBJ, "", "",
47
54
            {
48
54
                {RPCResult::Type::NUM, "feerate", /*optional=*/true, "estimate fee rate in " + CURRENCY_UNIT + "/kvB (only present if no errors were encountered)"},
49
54
                {RPCResult::Type::ARR, "errors", /*optional=*/true, "Errors encountered during processing (if there are any)",
50
54
                    {
51
54
                        {RPCResult::Type::STR, "", "error"},
52
54
                    }},
53
54
                {RPCResult::Type::NUM, "blocks", "block number where estimate was found\n"
54
54
                "The request target will be clamped between 2 and the highest target\n"
55
54
                "fee estimation is able to return based on how long it has been running.\n"
56
54
                "An error is returned if not enough transactions and blocks\n"
57
54
                "have been observed to make an estimate for any number of blocks."},
58
54
        }},
59
54
        RPCExamples{
60
54
            HelpExampleCli("estimatesmartfee", "6") +
61
54
            HelpExampleRpc("estimatesmartfee", "6")
62
54
        },
63
54
        [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
64
54
        {
65
0
            CBlockPolicyEstimator& fee_estimator = EnsureAnyFeeEstimator(request.context);
66
0
            const NodeContext& node = EnsureAnyNodeContext(request.context);
67
0
            const CTxMemPool& mempool = EnsureMemPool(node);
68
69
0
            CHECK_NONFATAL(mempool.m_opts.signals)->SyncWithValidationInterfaceQueue();
70
0
            unsigned int max_target = fee_estimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
71
0
            unsigned int conf_target = ParseConfirmTarget(request.params[0], max_target);
72
0
            FeeEstimateMode fee_mode;
73
0
            if (!FeeModeFromString(self.Arg<std::string_view>("estimate_mode"), fee_mode)) {
  Branch (73:17): [True: 0, False: 0]
74
0
                throw JSONRPCError(RPC_INVALID_PARAMETER, InvalidEstimateModeErrorMessage());
75
0
            }
76
77
0
            UniValue result(UniValue::VOBJ);
78
0
            UniValue errors(UniValue::VARR);
79
0
            FeeCalculation feeCalc;
80
0
            bool conservative{fee_mode == FeeEstimateMode::CONSERVATIVE};
81
0
            CFeeRate feeRate{fee_estimator.estimateSmartFee(conf_target, &feeCalc, conservative)};
82
0
            if (feeRate != CFeeRate(0)) {
  Branch (82:17): [True: 0, False: 0]
83
0
                CFeeRate min_mempool_feerate{mempool.GetMinFee()};
84
0
                CFeeRate min_relay_feerate{mempool.m_opts.min_relay_feerate};
85
0
                feeRate = std::max({feeRate, min_mempool_feerate, min_relay_feerate});
86
0
                result.pushKV("feerate", ValueFromAmount(feeRate.GetFeePerK()));
87
0
            } else {
88
0
                errors.push_back("Insufficient data or no feerate found");
89
0
                result.pushKV("errors", std::move(errors));
90
0
            }
91
0
            result.pushKV("blocks", feeCalc.returnedTarget);
92
0
            return result;
93
0
        },
94
54
    };
95
54
}
96
97
static std::vector<RPCResult> FeeRateBucketDoc(bool elide = false)
98
324
{
99
324
    auto fields = std::vector<RPCResult>{
100
324
        {RPCResult::Type::NUM, "startrange", "start of feerate range"},
101
324
        {RPCResult::Type::NUM, "endrange", "end of feerate range"},
102
324
        {RPCResult::Type::NUM, "withintarget", "number of txs over history horizon in the feerate range that were confirmed within target"},
103
324
        {RPCResult::Type::NUM, "totalconfirmed", "number of txs over history horizon in the feerate range that were confirmed at any point"},
104
324
        {RPCResult::Type::NUM, "inmempool", "current number of txs in mempool in the feerate range unconfirmed for at least target blocks"},
105
324
        {RPCResult::Type::NUM, "leftmempool", "number of txs over history horizon in the feerate range that left mempool unconfirmed after target"},
106
324
    };
107
324
    return elide ? ElideGroup(std::move(fields)) : fields;
  Branch (107:12): [True: 162, False: 162]
108
324
}
109
110
static std::vector<RPCResult> FeeEstimateHorizonDoc(bool elide = false)
111
162
{
112
162
    auto fields = std::vector<RPCResult>{
113
162
        {RPCResult::Type::NUM, "feerate", /*optional=*/true, "estimate fee rate in " + CURRENCY_UNIT + "/kvB"},
114
162
        {RPCResult::Type::NUM, "decay", "exponential decay (per block) for historical moving average of confirmation data"},
115
162
        {RPCResult::Type::NUM, "scale", "The resolution of confirmation targets at this time horizon"},
116
162
        {RPCResult::Type::OBJ, "pass", /*optional=*/true, "information about the lowest range of feerates to succeed in meeting the threshold", FeeRateBucketDoc()},
117
162
        {RPCResult::Type::OBJ, "fail", /*optional=*/true, "information about the highest range of feerates to fail to meet the threshold", FeeRateBucketDoc(/*elide=*/true)},
118
162
        {RPCResult::Type::ARR, "errors", /*optional=*/true, "Errors encountered during processing (if there are any)",
119
162
        {
120
162
            {RPCResult::Type::STR, "error", ""},
121
162
        }},
122
162
    };
123
162
    return elide ? ElideGroup(std::move(fields)) : fields;
  Branch (123:12): [True: 108, False: 54]
124
162
}
125
126
static RPCMethod estimaterawfee()
127
54
{
128
54
    return RPCMethod{
129
54
        "estimaterawfee",
130
54
        "WARNING: This interface is unstable and may disappear or change!\n"
131
54
        "\nWARNING: This is an advanced API call that is tightly coupled to the specific\n"
132
54
        "implementation of fee estimation. The parameters it can be called with\n"
133
54
        "and the results it returns will change if the internal implementation changes.\n"
134
54
        "\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
135
54
        "confirmation within conf_target blocks if possible. Uses virtual transaction size as\n"
136
54
        "defined in BIP 141 (witness data is discounted).\n",
137
54
        {
138
54
            {"conf_target", RPCArg::Type::NUM, RPCArg::Optional::NO, "Confirmation target in blocks (1 - 1008)"},
139
54
            {"threshold", RPCArg::Type::NUM, RPCArg::Default{0.95}, "The proportion of transactions in a given feerate range that must have been\n"
140
54
            "confirmed within conf_target in order to consider those feerates as high enough and proceed to check\n"
141
54
            "lower buckets."},
142
54
        },
143
54
        RPCResult{
144
54
            RPCResult::Type::OBJ, "", "Results are returned for any horizon which tracks blocks up to the confirmation target",
145
54
            {
146
54
                {RPCResult::Type::OBJ, "short", /*optional=*/true, "estimate for short time horizon",
147
54
                    FeeEstimateHorizonDoc()},
148
54
                {RPCResult::Type::OBJ, "medium", /*optional=*/true, "estimate for medium time horizon",
149
54
                    FeeEstimateHorizonDoc(/*elide=*/true)},
150
54
                {RPCResult::Type::OBJ, "long", /*optional=*/true, "estimate for long time horizon",
151
54
                    FeeEstimateHorizonDoc(/*elide=*/true)},
152
54
            }},
153
54
        RPCExamples{
154
54
            HelpExampleCli("estimaterawfee", "6 0.9")
155
54
        },
156
54
        [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
157
54
        {
158
0
            CBlockPolicyEstimator& fee_estimator = EnsureAnyFeeEstimator(request.context);
159
0
            const NodeContext& node = EnsureAnyNodeContext(request.context);
160
161
0
            CHECK_NONFATAL(node.validation_signals)->SyncWithValidationInterfaceQueue();
162
0
            unsigned int max_target = fee_estimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
163
0
            unsigned int conf_target = ParseConfirmTarget(request.params[0], max_target);
164
0
            double threshold = 0.95;
165
0
            if (!request.params[1].isNull()) {
  Branch (165:17): [True: 0, False: 0]
166
0
                threshold = request.params[1].get_real();
167
0
            }
168
0
            if (threshold < 0 || threshold > 1) {
  Branch (168:17): [True: 0, False: 0]
  Branch (168:34): [True: 0, False: 0]
169
0
                throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid threshold");
170
0
            }
171
172
0
            UniValue result(UniValue::VOBJ);
173
174
0
            for (const FeeEstimateHorizon horizon : ALL_FEE_ESTIMATE_HORIZONS) {
  Branch (174:51): [True: 0, False: 0]
175
0
                CFeeRate feeRate;
176
0
                EstimationResult buckets;
177
178
                // Only output results for horizons which track the target
179
0
                if (conf_target > fee_estimator.HighestTargetTracked(horizon)) continue;
  Branch (179:21): [True: 0, False: 0]
180
181
0
                feeRate = fee_estimator.estimateRawFee(conf_target, threshold, horizon, &buckets);
182
0
                UniValue horizon_result(UniValue::VOBJ);
183
0
                UniValue errors(UniValue::VARR);
184
0
                UniValue passbucket(UniValue::VOBJ);
185
0
                passbucket.pushKV("startrange", round(buckets.pass.start));
186
0
                passbucket.pushKV("endrange", round(buckets.pass.end));
187
0
                passbucket.pushKV("withintarget", round(buckets.pass.withinTarget * 100.0) / 100.0);
188
0
                passbucket.pushKV("totalconfirmed", round(buckets.pass.totalConfirmed * 100.0) / 100.0);
189
0
                passbucket.pushKV("inmempool", round(buckets.pass.inMempool * 100.0) / 100.0);
190
0
                passbucket.pushKV("leftmempool", round(buckets.pass.leftMempool * 100.0) / 100.0);
191
0
                UniValue failbucket(UniValue::VOBJ);
192
0
                failbucket.pushKV("startrange", round(buckets.fail.start));
193
0
                failbucket.pushKV("endrange", round(buckets.fail.end));
194
0
                failbucket.pushKV("withintarget", round(buckets.fail.withinTarget * 100.0) / 100.0);
195
0
                failbucket.pushKV("totalconfirmed", round(buckets.fail.totalConfirmed * 100.0) / 100.0);
196
0
                failbucket.pushKV("inmempool", round(buckets.fail.inMempool * 100.0) / 100.0);
197
0
                failbucket.pushKV("leftmempool", round(buckets.fail.leftMempool * 100.0) / 100.0);
198
199
                // CFeeRate(0) is used to indicate error as a return value from estimateRawFee
200
0
                if (feeRate != CFeeRate(0)) {
  Branch (200:21): [True: 0, False: 0]
201
0
                    horizon_result.pushKV("feerate", ValueFromAmount(feeRate.GetFeePerK()));
202
0
                    horizon_result.pushKV("decay", buckets.decay);
203
0
                    horizon_result.pushKV("scale", buckets.scale);
204
0
                    horizon_result.pushKV("pass", std::move(passbucket));
205
                    // buckets.fail.start == -1 indicates that all buckets passed, there is no fail bucket to output
206
0
                    if (buckets.fail.start != -1) horizon_result.pushKV("fail", std::move(failbucket));
  Branch (206:25): [True: 0, False: 0]
207
0
                } else {
208
                    // Output only information that is still meaningful in the event of error
209
0
                    horizon_result.pushKV("decay", buckets.decay);
210
0
                    horizon_result.pushKV("scale", buckets.scale);
211
0
                    horizon_result.pushKV("fail", std::move(failbucket));
212
0
                    errors.push_back("Insufficient data or no feerate found which meets threshold");
213
0
                    horizon_result.pushKV("errors", std::move(errors));
214
0
                }
215
0
                result.pushKV(StringForFeeEstimateHorizon(horizon), std::move(horizon_result));
216
0
            }
217
0
            return result;
218
0
        },
219
54
    };
220
54
}
221
222
void RegisterFeeRPCCommands(CRPCTable& t)
223
27
{
224
27
    static const CRPCCommand commands[]{
225
27
        {"util", &estimatesmartfee},
226
27
        {"hidden", &estimaterawfee},
227
27
    };
228
54
    for (const auto& c : commands) {
  Branch (228:24): [True: 54, False: 27]
229
54
        t.appendCommand(c.name, &c);
230
54
    }
231
27
}