Coverage Report

Created: 2025-06-10 13:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/bitcoin/src/wallet/rpc/transactions.cpp
Line
Count
Source
1
// Copyright (c) 2011-present The Bitcoin Core developers
2
// Distributed under the MIT software license, see the accompanying
3
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5
#include <core_io.h>
6
#include <key_io.h>
7
#include <policy/rbf.h>
8
#include <rpc/util.h>
9
#include <rpc/blockchain.h>
10
#include <util/transaction_identifier.h>
11
#include <util/vector.h>
12
#include <wallet/receive.h>
13
#include <wallet/rpc/util.h>
14
#include <wallet/wallet.h>
15
16
using interfaces::FoundBlock;
17
18
namespace wallet {
19
static void WalletTxToJSON(const CWallet& wallet, const CWalletTx& wtx, UniValue& entry)
20
    EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
21
0
{
22
0
    interfaces::Chain& chain = wallet.chain();
23
0
    int confirms = wallet.GetTxDepthInMainChain(wtx);
24
0
    entry.pushKV("confirmations", confirms);
25
0
    if (wtx.IsCoinBase())
  Branch (25:9): [True: 0, False: 0]
26
0
        entry.pushKV("generated", true);
27
0
    if (auto* conf = wtx.state<TxStateConfirmed>())
  Branch (27:15): [True: 0, False: 0]
28
0
    {
29
0
        entry.pushKV("blockhash", conf->confirmed_block_hash.GetHex());
30
0
        entry.pushKV("blockheight", conf->confirmed_block_height);
31
0
        entry.pushKV("blockindex", conf->position_in_block);
32
0
        int64_t block_time;
33
0
        CHECK_NONFATAL(chain.findBlock(conf->confirmed_block_hash, FoundBlock().time(block_time)));
34
0
        entry.pushKV("blocktime", block_time);
35
0
    } else {
36
0
        entry.pushKV("trusted", CachedTxIsTrusted(wallet, wtx));
37
0
    }
38
0
    entry.pushKV("txid", wtx.GetHash().GetHex());
39
0
    entry.pushKV("wtxid", wtx.GetWitnessHash().GetHex());
40
0
    UniValue conflicts(UniValue::VARR);
41
0
    for (const Txid& conflict : wallet.GetTxConflicts(wtx))
  Branch (41:31): [True: 0, False: 0]
42
0
        conflicts.push_back(conflict.GetHex());
43
0
    entry.pushKV("walletconflicts", std::move(conflicts));
44
0
    UniValue mempool_conflicts(UniValue::VARR);
45
0
    for (const Txid& mempool_conflict : wtx.mempool_conflicts)
  Branch (45:39): [True: 0, False: 0]
46
0
        mempool_conflicts.push_back(mempool_conflict.GetHex());
47
0
    entry.pushKV("mempoolconflicts", std::move(mempool_conflicts));
48
0
    entry.pushKV("time", wtx.GetTxTime());
49
0
    entry.pushKV("timereceived", int64_t{wtx.nTimeReceived});
50
51
    // Add opt-in RBF status
52
0
    std::string rbfStatus = "no";
53
0
    if (confirms <= 0) {
  Branch (53:9): [True: 0, False: 0]
54
0
        RBFTransactionState rbfState = chain.isRBFOptIn(*wtx.tx);
55
0
        if (rbfState == RBFTransactionState::UNKNOWN)
  Branch (55:13): [True: 0, False: 0]
56
0
            rbfStatus = "unknown";
57
0
        else if (rbfState == RBFTransactionState::REPLACEABLE_BIP125)
  Branch (57:18): [True: 0, False: 0]
58
0
            rbfStatus = "yes";
59
0
    }
60
0
    entry.pushKV("bip125-replaceable", rbfStatus);
61
62
0
    for (const std::pair<const std::string, std::string>& item : wtx.mapValue)
  Branch (62:64): [True: 0, False: 0]
63
0
        entry.pushKV(item.first, item.second);
64
0
}
65
66
struct tallyitem
67
{
68
    CAmount nAmount{0};
69
    int nConf{std::numeric_limits<int>::max()};
70
    std::vector<Txid> txids;
71
    bool fIsWatchonly{false};
72
0
    tallyitem() = default;
73
};
74
75
static UniValue ListReceived(const CWallet& wallet, const UniValue& params, const bool by_label, const bool include_immature_coinbase) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
76
0
{
77
    // Minimum confirmations
78
0
    int nMinDepth = 1;
79
0
    if (!params[0].isNull())
  Branch (79:9): [True: 0, False: 0]
80
0
        nMinDepth = params[0].getInt<int>();
81
82
    // Whether to include empty labels
83
0
    bool fIncludeEmpty = false;
84
0
    if (!params[1].isNull())
  Branch (84:9): [True: 0, False: 0]
85
0
        fIncludeEmpty = params[1].get_bool();
86
87
0
    isminefilter filter = ISMINE_SPENDABLE;
88
89
0
    if (ParseIncludeWatchonly(params[2], wallet)) {
  Branch (89:9): [True: 0, False: 0]
90
0
        filter |= ISMINE_WATCH_ONLY;
91
0
    }
92
93
0
    std::optional<CTxDestination> filtered_address{std::nullopt};
94
0
    if (!by_label && !params[3].isNull() && !params[3].get_str().empty()) {
  Branch (94:9): [True: 0, False: 0]
  Branch (94:22): [True: 0, False: 0]
  Branch (94:45): [True: 0, False: 0]
95
0
        if (!IsValidDestinationString(params[3].get_str())) {
  Branch (95:13): [True: 0, False: 0]
96
0
            throw JSONRPCError(RPC_WALLET_ERROR, "address_filter parameter was invalid");
97
0
        }
98
0
        filtered_address = DecodeDestination(params[3].get_str());
99
0
    }
100
101
    // Tally
102
0
    std::map<CTxDestination, tallyitem> mapTally;
103
0
    for (const auto& [_, wtx] : wallet.mapWallet) {
  Branch (103:31): [True: 0, False: 0]
104
105
0
        int nDepth = wallet.GetTxDepthInMainChain(wtx);
106
0
        if (nDepth < nMinDepth)
  Branch (106:13): [True: 0, False: 0]
107
0
            continue;
108
109
        // Coinbase with less than 1 confirmation is no longer in the main chain
110
0
        if ((wtx.IsCoinBase() && (nDepth < 1))
  Branch (110:14): [True: 0, False: 0]
  Branch (110:34): [True: 0, False: 0]
111
0
            || (wallet.IsTxImmatureCoinBase(wtx) && !include_immature_coinbase)) {
  Branch (111:17): [True: 0, False: 0]
  Branch (111:53): [True: 0, False: 0]
112
0
            continue;
113
0
        }
114
115
0
        for (const CTxOut& txout : wtx.tx->vout) {
  Branch (115:34): [True: 0, False: 0]
116
0
            CTxDestination address;
117
0
            if (!ExtractDestination(txout.scriptPubKey, address))
  Branch (117:17): [True: 0, False: 0]
118
0
                continue;
119
120
0
            if (filtered_address && !(filtered_address == address)) {
  Branch (120:17): [True: 0, False: 0]
  Branch (120:37): [True: 0, False: 0]
121
0
                continue;
122
0
            }
123
124
0
            isminefilter mine = wallet.IsMine(address);
125
0
            if (!(mine & filter))
  Branch (125:17): [True: 0, False: 0]
126
0
                continue;
127
128
0
            tallyitem& item = mapTally[address];
129
0
            item.nAmount += txout.nValue;
130
0
            item.nConf = std::min(item.nConf, nDepth);
131
0
            item.txids.push_back(wtx.GetHash());
132
0
            if (mine & ISMINE_WATCH_ONLY)
  Branch (132:17): [True: 0, False: 0]
133
0
                item.fIsWatchonly = true;
134
0
        }
135
0
    }
136
137
    // Reply
138
0
    UniValue ret(UniValue::VARR);
139
0
    std::map<std::string, tallyitem> label_tally;
140
141
0
    const auto& func = [&](const CTxDestination& address, const std::string& label, bool is_change, const std::optional<AddressPurpose>& purpose) {
142
0
        if (is_change) return; // no change addresses
  Branch (142:13): [True: 0, False: 0]
143
144
0
        auto it = mapTally.find(address);
145
0
        if (it == mapTally.end() && !fIncludeEmpty)
  Branch (145:13): [True: 0, False: 0]
  Branch (145:13): [True: 0, False: 0]
  Branch (145:37): [True: 0, False: 0]
146
0
            return;
147
148
0
        CAmount nAmount = 0;
149
0
        int nConf = std::numeric_limits<int>::max();
150
0
        bool fIsWatchonly = false;
151
0
        if (it != mapTally.end()) {
  Branch (151:13): [True: 0, False: 0]
152
0
            nAmount = (*it).second.nAmount;
153
0
            nConf = (*it).second.nConf;
154
0
            fIsWatchonly = (*it).second.fIsWatchonly;
155
0
        }
156
157
0
        if (by_label) {
  Branch (157:13): [True: 0, False: 0]
158
0
            tallyitem& _item = label_tally[label];
159
0
            _item.nAmount += nAmount;
160
0
            _item.nConf = std::min(_item.nConf, nConf);
161
0
            _item.fIsWatchonly = fIsWatchonly;
162
0
        } else {
163
0
            UniValue obj(UniValue::VOBJ);
164
0
            if (fIsWatchonly) obj.pushKV("involvesWatchonly", true);
  Branch (164:17): [True: 0, False: 0]
165
0
            obj.pushKV("address",       EncodeDestination(address));
166
0
            obj.pushKV("amount",        ValueFromAmount(nAmount));
167
0
            obj.pushKV("confirmations", (nConf == std::numeric_limits<int>::max() ? 0 : nConf));
  Branch (167:42): [True: 0, False: 0]
168
0
            obj.pushKV("label", label);
169
0
            UniValue transactions(UniValue::VARR);
170
0
            if (it != mapTally.end()) {
  Branch (170:17): [True: 0, False: 0]
171
0
                for (const Txid& _item : (*it).second.txids) {
  Branch (171:40): [True: 0, False: 0]
172
0
                    transactions.push_back(_item.GetHex());
173
0
                }
174
0
            }
175
0
            obj.pushKV("txids", std::move(transactions));
176
0
            ret.push_back(std::move(obj));
177
0
        }
178
0
    };
179
180
0
    if (filtered_address) {
  Branch (180:9): [True: 0, False: 0]
181
0
        const auto& entry = wallet.FindAddressBookEntry(*filtered_address, /*allow_change=*/false);
182
0
        if (entry) func(*filtered_address, entry->GetLabel(), entry->IsChange(), entry->purpose);
  Branch (182:13): [True: 0, False: 0]
183
0
    } else {
184
        // No filtered addr, walk-through the addressbook entry
185
0
        wallet.ForEachAddrBookEntry(func);
186
0
    }
187
188
0
    if (by_label) {
  Branch (188:9): [True: 0, False: 0]
189
0
        for (const auto& entry : label_tally) {
  Branch (189:32): [True: 0, False: 0]
190
0
            CAmount nAmount = entry.second.nAmount;
191
0
            int nConf = entry.second.nConf;
192
0
            UniValue obj(UniValue::VOBJ);
193
0
            if (entry.second.fIsWatchonly)
  Branch (193:17): [True: 0, False: 0]
194
0
                obj.pushKV("involvesWatchonly", true);
195
0
            obj.pushKV("amount",        ValueFromAmount(nAmount));
196
0
            obj.pushKV("confirmations", (nConf == std::numeric_limits<int>::max() ? 0 : nConf));
  Branch (196:42): [True: 0, False: 0]
197
0
            obj.pushKV("label",         entry.first);
198
0
            ret.push_back(std::move(obj));
199
0
        }
200
0
    }
201
202
0
    return ret;
203
0
}
204
205
RPCHelpMan listreceivedbyaddress()
206
22.1k
{
207
22.1k
    return RPCHelpMan{
208
22.1k
        "listreceivedbyaddress",
209
22.1k
        "List balances by receiving address.\n",
210
22.1k
                {
211
22.1k
                    {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."},
212
22.1k
                    {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include addresses that haven't received any payments."},
213
22.1k
                    {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"},
214
22.1k
                    {"address_filter", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "If present and non-empty, only return information on this address."},
215
22.1k
                    {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
216
22.1k
                },
217
22.1k
                RPCResult{
218
22.1k
                    RPCResult::Type::ARR, "", "",
219
22.1k
                    {
220
22.1k
                        {RPCResult::Type::OBJ, "", "",
221
22.1k
                        {
222
22.1k
                            {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
223
22.1k
                            {RPCResult::Type::STR, "address", "The receiving address"},
224
22.1k
                            {RPCResult::Type::STR_AMOUNT, "amount", "The total amount in " + CURRENCY_UNIT + " received by the address"},
225
22.1k
                            {RPCResult::Type::NUM, "confirmations", "The number of confirmations of the most recent transaction included"},
226
22.1k
                            {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""},
227
22.1k
                            {RPCResult::Type::ARR, "txids", "",
228
22.1k
                            {
229
22.1k
                                {RPCResult::Type::STR_HEX, "txid", "The ids of transactions received with the address"},
230
22.1k
                            }},
231
22.1k
                        }},
232
22.1k
                    }
233
22.1k
                },
234
22.1k
                RPCExamples{
235
22.1k
                    HelpExampleCli("listreceivedbyaddress", "")
236
22.1k
            + HelpExampleCli("listreceivedbyaddress", "6 true")
237
22.1k
            + HelpExampleCli("listreceivedbyaddress", "6 true true \"\" true")
238
22.1k
            + HelpExampleRpc("listreceivedbyaddress", "6, true, true")
239
22.1k
            + HelpExampleRpc("listreceivedbyaddress", "6, true, true, \"" + EXAMPLE_ADDRESS[0] + "\", true")
240
22.1k
                },
241
22.1k
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
242
22.1k
{
243
0
    const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
244
0
    if (!pwallet) return UniValue::VNULL;
  Branch (244:9): [True: 0, False: 0]
245
246
    // Make sure the results are valid at least up to the most recent block
247
    // the user could have gotten from another RPC command prior to now
248
0
    pwallet->BlockUntilSyncedToCurrentChain();
249
250
0
    const bool include_immature_coinbase{request.params[4].isNull() ? false : request.params[4].get_bool()};
  Branch (250:42): [True: 0, False: 0]
251
252
0
    LOCK(pwallet->cs_wallet);
253
254
0
    return ListReceived(*pwallet, request.params, false, include_immature_coinbase);
255
0
},
256
22.1k
    };
257
22.1k
}
258
259
RPCHelpMan listreceivedbylabel()
260
22.1k
{
261
22.1k
    return RPCHelpMan{
262
22.1k
        "listreceivedbylabel",
263
22.1k
        "List received transactions by label.\n",
264
22.1k
                {
265
22.1k
                    {"minconf", RPCArg::Type::NUM, RPCArg::Default{1}, "The minimum number of confirmations before payments are included."},
266
22.1k
                    {"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to include labels that haven't received any payments."},
267
22.1k
                    {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Whether to include watch-only addresses (see 'importaddress')"},
268
22.1k
                    {"include_immature_coinbase", RPCArg::Type::BOOL, RPCArg::Default{false}, "Include immature coinbase transactions."},
269
22.1k
                },
270
22.1k
                RPCResult{
271
22.1k
                    RPCResult::Type::ARR, "", "",
272
22.1k
                    {
273
22.1k
                        {RPCResult::Type::OBJ, "", "",
274
22.1k
                        {
275
22.1k
                            {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction"},
276
22.1k
                            {RPCResult::Type::STR_AMOUNT, "amount", "The total amount received by addresses with this label"},
277
22.1k
                            {RPCResult::Type::NUM, "confirmations", "The number of confirmations of the most recent transaction included"},
278
22.1k
                            {RPCResult::Type::STR, "label", "The label of the receiving address. The default label is \"\""},
279
22.1k
                        }},
280
22.1k
                    }
281
22.1k
                },
282
22.1k
                RPCExamples{
283
22.1k
                    HelpExampleCli("listreceivedbylabel", "")
284
22.1k
            + HelpExampleCli("listreceivedbylabel", "6 true")
285
22.1k
            + HelpExampleRpc("listreceivedbylabel", "6, true, true, true")
286
22.1k
                },
287
22.1k
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
288
22.1k
{
289
0
    const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
290
0
    if (!pwallet) return UniValue::VNULL;
  Branch (290:9): [True: 0, False: 0]
291
292
    // Make sure the results are valid at least up to the most recent block
293
    // the user could have gotten from another RPC command prior to now
294
0
    pwallet->BlockUntilSyncedToCurrentChain();
295
296
0
    const bool include_immature_coinbase{request.params[3].isNull() ? false : request.params[3].get_bool()};
  Branch (296:42): [True: 0, False: 0]
297
298
0
    LOCK(pwallet->cs_wallet);
299
300
0
    return ListReceived(*pwallet, request.params, true, include_immature_coinbase);
301
0
},
302
22.1k
    };
303
22.1k
}
304
305
static void MaybePushAddress(UniValue & entry, const CTxDestination &dest)
306
0
{
307
0
    if (IsValidDestination(dest)) {
  Branch (307:9): [True: 0, False: 0]
308
0
        entry.pushKV("address", EncodeDestination(dest));
309
0
    }
310
0
}
311
312
/**
313
 * List transactions based on the given criteria.
314
 *
315
 * @param  wallet         The wallet.
316
 * @param  wtx            The wallet transaction.
317
 * @param  nMinDepth      The minimum confirmation depth.
318
 * @param  fLong          Whether to include the JSON version of the transaction.
319
 * @param  ret            The vector into which the result is stored.
320
 * @param  filter_ismine  The "is mine" filter flags.
321
 * @param  filter_label   Optional label string to filter incoming transactions.
322
 */
323
template <class Vec>
324
static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong,
325
                             Vec& ret, const isminefilter& filter_ismine, const std::optional<std::string>& filter_label,
326
                             bool include_change = false)
327
    EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
328
0
{
329
0
    CAmount nFee;
330
0
    std::list<COutputEntry> listReceived;
331
0
    std::list<COutputEntry> listSent;
332
333
0
    CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine, include_change);
334
335
0
    bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY);
336
337
    // Sent
338
0
    if (!filter_label.has_value())
  Branch (338:9): [True: 0, False: 0]
  Branch (338:9): [True: 0, False: 0]
339
0
    {
340
0
        for (const COutputEntry& s : listSent)
  Branch (340:36): [True: 0, False: 0]
  Branch (340:36): [True: 0, False: 0]
341
0
        {
342
0
            UniValue entry(UniValue::VOBJ);
343
0
            if (involvesWatchonly || (wallet.IsMine(s.destination) & ISMINE_WATCH_ONLY)) {
  Branch (343:17): [True: 0, False: 0]
  Branch (343:38): [True: 0, False: 0]
  Branch (343:17): [True: 0, False: 0]
  Branch (343:38): [True: 0, False: 0]
344
0
                entry.pushKV("involvesWatchonly", true);
345
0
            }
346
0
            MaybePushAddress(entry, s.destination);
347
0
            entry.pushKV("category", "send");
348
0
            entry.pushKV("amount", ValueFromAmount(-s.amount));
349
0
            const auto* address_book_entry = wallet.FindAddressBookEntry(s.destination);
350
0
            if (address_book_entry) {
  Branch (350:17): [True: 0, False: 0]
  Branch (350:17): [True: 0, False: 0]
351
0
                entry.pushKV("label", address_book_entry->GetLabel());
352
0
            }
353
0
            entry.pushKV("vout", s.vout);
354
0
            entry.pushKV("fee", ValueFromAmount(-nFee));
355
0
            if (fLong)
  Branch (355:17): [True: 0, False: 0]
  Branch (355:17): [True: 0, False: 0]
356
0
                WalletTxToJSON(wallet, wtx, entry);
357
0
            entry.pushKV("abandoned", wtx.isAbandoned());
358
0
            ret.push_back(std::move(entry));
359
0
        }
360
0
    }
361
362
    // Received
363
0
    if (listReceived.size() > 0 && wallet.GetTxDepthInMainChain(wtx) >= nMinDepth) {
  Branch (363:9): [True: 0, False: 0]
  Branch (363:36): [True: 0, False: 0]
  Branch (363:9): [True: 0, False: 0]
  Branch (363:36): [True: 0, False: 0]
364
0
        for (const COutputEntry& r : listReceived)
  Branch (364:36): [True: 0, False: 0]
  Branch (364:36): [True: 0, False: 0]
365
0
        {
366
0
            std::string label;
367
0
            const auto* address_book_entry = wallet.FindAddressBookEntry(r.destination);
368
0
            if (address_book_entry) {
  Branch (368:17): [True: 0, False: 0]
  Branch (368:17): [True: 0, False: 0]
369
0
                label = address_book_entry->GetLabel();
370
0
            }
371
0
            if (filter_label.has_value() && label != filter_label.value()) {
  Branch (371:17): [True: 0, False: 0]
  Branch (371:45): [True: 0, False: 0]
  Branch (371:17): [True: 0, False: 0]
  Branch (371:45): [True: 0, False: 0]
372
0
                continue;
373
0
            }
374
0
            UniValue entry(UniValue::VOBJ);
375
0
            if (involvesWatchonly || (wallet.IsMine(r.destination) & ISMINE_WATCH_ONLY)) {
  Branch (375:17): [True: 0, False: 0]
  Branch (375:38): [True: 0, False: 0]
  Branch (375:17): [True: 0, False: 0]
  Branch (375:38): [True: 0, False: 0]
376
0
                entry.pushKV("involvesWatchonly", true);
377
0
            }
378
0
            MaybePushAddress(entry, r.destination);
379
0
            PushParentDescriptors(wallet, wtx.tx->vout.at(r.vout).scriptPubKey, entry);
380
0
            if (wtx.IsCoinBase())
  Branch (380:17): [True: 0, False: 0]
  Branch (380:17): [True: 0, False: 0]
381
0
            {
382
0
                if (wallet.GetTxDepthInMainChain(wtx) < 1)
  Branch (382:21): [True: 0, False: 0]
  Branch (382:21): [True: 0, False: 0]
383
0
                    entry.pushKV("category", "orphan");
384
0
                else if (wallet.IsTxImmatureCoinBase(wtx))
  Branch (384:26): [True: 0, False: 0]
  Branch (384:26): [True: 0, False: 0]
385
0
                    entry.pushKV("category", "immature");
386
0
                else
387
0
                    entry.pushKV("category", "generate");
388
0
            }
389
0
            else
390
0
            {
391
0
                entry.pushKV("category", "receive");
392
0
            }
393
0
            entry.pushKV("amount", ValueFromAmount(r.amount));
394
0
            if (address_book_entry) {
  Branch (394:17): [True: 0, False: 0]
  Branch (394:17): [True: 0, False: 0]
395
0
                entry.pushKV("label", label);
396
0
            }
397
0
            entry.pushKV("vout", r.vout);
398
0
            entry.pushKV("abandoned", wtx.isAbandoned());
399
0
            if (fLong)
  Branch (399:17): [True: 0, False: 0]
  Branch (399:17): [True: 0, False: 0]
400
0
                WalletTxToJSON(wallet, wtx, entry);
401
0
            ret.push_back(std::move(entry));
402
0
        }
403
0
    }
404
0
}
Unexecuted instantiation: transactions.cpp:void wallet::ListTransactions<std::vector<UniValue, std::allocator<UniValue> > >(wallet::CWallet const&, wallet::CWalletTx const&, int, bool, std::vector<UniValue, std::allocator<UniValue> >&, unsigned int const&, std::optional<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > const&, bool)
Unexecuted instantiation: transactions.cpp:void wallet::ListTransactions<UniValue>(wallet::CWallet const&, wallet::CWalletTx const&, int, bool, UniValue&, unsigned int const&, std::optional<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > const&, bool)
405
406
407
static std::vector<RPCResult> TransactionDescriptionString()
408
66.5k
{
409
66.5k
    return{{RPCResult::Type::NUM, "confirmations", "The number of confirmations for the transaction. Negative confirmations means the\n"
410
66.5k
               "transaction conflicted that many blocks ago."},
411
66.5k
           {RPCResult::Type::BOOL, "generated", /*optional=*/true, "Only present if the transaction's only input is a coinbase one."},
412
66.5k
           {RPCResult::Type::BOOL, "trusted", /*optional=*/true, "Whether we consider the transaction to be trusted and safe to spend from.\n"
413
66.5k
                "Only present when the transaction has 0 confirmations (or negative confirmations, if conflicted)."},
414
66.5k
           {RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "The block hash containing the transaction."},
415
66.5k
           {RPCResult::Type::NUM, "blockheight", /*optional=*/true, "The block height containing the transaction."},
416
66.5k
           {RPCResult::Type::NUM, "blockindex", /*optional=*/true, "The index of the transaction in the block that includes it."},
417
66.5k
           {RPCResult::Type::NUM_TIME, "blocktime", /*optional=*/true, "The block time expressed in " + UNIX_EPOCH_TIME + "."},
418
66.5k
           {RPCResult::Type::STR_HEX, "txid", "The transaction id."},
419
66.5k
           {RPCResult::Type::STR_HEX, "wtxid", "The hash of serialized transaction, including witness data."},
420
66.5k
           {RPCResult::Type::ARR, "walletconflicts", "Confirmed transactions that have been detected by the wallet to conflict with this transaction.",
421
66.5k
           {
422
66.5k
               {RPCResult::Type::STR_HEX, "txid", "The transaction id."},
423
66.5k
           }},
424
66.5k
           {RPCResult::Type::STR_HEX, "replaced_by_txid", /*optional=*/true, "Only if 'category' is 'send'. The txid if this tx was replaced."},
425
66.5k
           {RPCResult::Type::STR_HEX, "replaces_txid", /*optional=*/true, "Only if 'category' is 'send'. The txid if this tx replaces another."},
426
66.5k
           {RPCResult::Type::ARR, "mempoolconflicts", "Transactions in the mempool that directly conflict with either this transaction or an ancestor transaction",
427
66.5k
           {
428
66.5k
               {RPCResult::Type::STR_HEX, "txid", "The transaction id."},
429
66.5k
           }},
430
66.5k
           {RPCResult::Type::STR, "to", /*optional=*/true, "If a comment to is associated with the transaction."},
431
66.5k
           {RPCResult::Type::NUM_TIME, "time", "The transaction time expressed in " + UNIX_EPOCH_TIME + "."},
432
66.5k
           {RPCResult::Type::NUM_TIME, "timereceived", "The time received expressed in " + UNIX_EPOCH_TIME + "."},
433
66.5k
           {RPCResult::Type::STR, "comment", /*optional=*/true, "If a comment is associated with the transaction, only present if not empty."},
434
66.5k
           {RPCResult::Type::STR, "bip125-replaceable", "(\"yes|no|unknown\") Whether this transaction signals BIP125 replaceability or has an unconfirmed ancestor signaling BIP125 replaceability.\n"
435
66.5k
               "May be unknown for unconfirmed transactions not in the mempool because their unconfirmed ancestors are unknown."},
436
66.5k
           {RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the output script of this coin.", {
437
66.5k
               {RPCResult::Type::STR, "desc", "The descriptor string."},
438
66.5k
           }},
439
66.5k
           };
440
66.5k
}
441
442
RPCHelpMan listtransactions()
443
22.1k
{
444
22.1k
    return RPCHelpMan{
445
22.1k
        "listtransactions",
446
22.1k
        "If a label name is provided, this will return only incoming transactions paying to addresses with the specified label.\n"
447
22.1k
                "\nReturns up to 'count' most recent transactions skipping the first 'from' transactions.\n",
448
22.1k
                {
449
22.1k
                    {"label", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "If set, should be a valid label name to return only incoming transactions\n"
450
22.1k
                          "with the specified label, or \"*\" to disable filtering and return all transactions."},
451
22.1k
                    {"count", RPCArg::Type::NUM, RPCArg::Default{10}, "The number of transactions to return"},
452
22.1k
                    {"skip", RPCArg::Type::NUM, RPCArg::Default{0}, "The number of transactions to skip"},
453
22.1k
                    {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"},
454
22.1k
                },
455
22.1k
                RPCResult{
456
22.1k
                    RPCResult::Type::ARR, "", "",
457
22.1k
                    {
458
22.1k
                        {RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
459
22.1k
                        {
460
22.1k
                            {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction."},
461
22.1k
                            {RPCResult::Type::STR, "address",  /*optional=*/true, "The bitcoin address of the transaction (not returned if the output does not have an address, e.g. OP_RETURN null data)."},
462
22.1k
                            {RPCResult::Type::STR, "category", "The transaction category.\n"
463
22.1k
                                "\"send\"                  Transactions sent.\n"
464
22.1k
                                "\"receive\"               Non-coinbase transactions received.\n"
465
22.1k
                                "\"generate\"              Coinbase transactions received with more than 100 confirmations.\n"
466
22.1k
                                "\"immature\"              Coinbase transactions received with 100 or fewer confirmations.\n"
467
22.1k
                                "\"orphan\"                Orphaned coinbase transactions received."},
468
22.1k
                            {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n"
469
22.1k
                                "for all other categories"},
470
22.1k
                            {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any"},
471
22.1k
                            {RPCResult::Type::NUM, "vout", "the vout value"},
472
22.1k
                            {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n"
473
22.1k
                                 "'send' category of transactions."},
474
22.1k
                        },
475
22.1k
                        TransactionDescriptionString()),
476
22.1k
                        {
477
22.1k
                            {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
478
22.1k
                        })},
479
22.1k
                    }
480
22.1k
                },
481
22.1k
                RPCExamples{
482
22.1k
            "\nList the most recent 10 transactions in the systems\n"
483
22.1k
            + HelpExampleCli("listtransactions", "") +
484
22.1k
            "\nList transactions 100 to 120\n"
485
22.1k
            + HelpExampleCli("listtransactions", "\"*\" 20 100") +
486
22.1k
            "\nAs a JSON-RPC call\n"
487
22.1k
            + HelpExampleRpc("listtransactions", "\"*\", 20, 100")
488
22.1k
                },
489
22.1k
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
490
22.1k
{
491
0
    const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
492
0
    if (!pwallet) return UniValue::VNULL;
  Branch (492:9): [True: 0, False: 0]
493
494
    // Make sure the results are valid at least up to the most recent block
495
    // the user could have gotten from another RPC command prior to now
496
0
    pwallet->BlockUntilSyncedToCurrentChain();
497
498
0
    std::optional<std::string> filter_label;
499
0
    if (!request.params[0].isNull() && request.params[0].get_str() != "*") {
  Branch (499:9): [True: 0, False: 0]
  Branch (499:40): [True: 0, False: 0]
500
0
        filter_label.emplace(LabelFromValue(request.params[0]));
501
0
        if (filter_label.value().empty()) {
  Branch (501:13): [True: 0, False: 0]
502
0
            throw JSONRPCError(RPC_INVALID_PARAMETER, "Label argument must be a valid label name or \"*\".");
503
0
        }
504
0
    }
505
0
    int nCount = 10;
506
0
    if (!request.params[1].isNull())
  Branch (506:9): [True: 0, False: 0]
507
0
        nCount = request.params[1].getInt<int>();
508
0
    int nFrom = 0;
509
0
    if (!request.params[2].isNull())
  Branch (509:9): [True: 0, False: 0]
510
0
        nFrom = request.params[2].getInt<int>();
511
0
    isminefilter filter = ISMINE_SPENDABLE;
512
513
0
    if (ParseIncludeWatchonly(request.params[3], *pwallet)) {
  Branch (513:9): [True: 0, False: 0]
514
0
        filter |= ISMINE_WATCH_ONLY;
515
0
    }
516
517
0
    if (nCount < 0)
  Branch (517:9): [True: 0, False: 0]
518
0
        throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative count");
519
0
    if (nFrom < 0)
  Branch (519:9): [True: 0, False: 0]
520
0
        throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative from");
521
522
0
    std::vector<UniValue> ret;
523
0
    {
524
0
        LOCK(pwallet->cs_wallet);
525
526
0
        const CWallet::TxItems & txOrdered = pwallet->wtxOrdered;
527
528
        // iterate backwards until we have nCount items to return:
529
0
        for (CWallet::TxItems::const_reverse_iterator it = txOrdered.rbegin(); it != txOrdered.rend(); ++it)
  Branch (529:80): [True: 0, False: 0]
530
0
        {
531
0
            CWalletTx *const pwtx = (*it).second;
532
0
            ListTransactions(*pwallet, *pwtx, 0, true, ret, filter, filter_label);
533
0
            if ((int)ret.size() >= (nCount+nFrom)) break;
  Branch (533:17): [True: 0, False: 0]
534
0
        }
535
0
    }
536
537
    // ret is newest to oldest
538
539
0
    if (nFrom > (int)ret.size())
  Branch (539:9): [True: 0, False: 0]
540
0
        nFrom = ret.size();
541
0
    if ((nFrom + nCount) > (int)ret.size())
  Branch (541:9): [True: 0, False: 0]
542
0
        nCount = ret.size() - nFrom;
543
544
0
    auto txs_rev_it{std::make_move_iterator(ret.rend())};
545
0
    UniValue result{UniValue::VARR};
546
0
    result.push_backV(txs_rev_it - nFrom - nCount, txs_rev_it - nFrom); // Return oldest to newest
547
0
    return result;
548
0
},
549
22.1k
    };
550
22.1k
}
551
552
RPCHelpMan listsinceblock()
553
22.1k
{
554
22.1k
    return RPCHelpMan{
555
22.1k
        "listsinceblock",
556
22.1k
        "Get all transactions in blocks since block [blockhash], or all transactions if omitted.\n"
557
22.1k
                "If \"blockhash\" is no longer a part of the main chain, transactions from the fork point onward are included.\n"
558
22.1k
                "Additionally, if include_removed is set, transactions affecting the wallet which were removed are returned in the \"removed\" array.\n",
559
22.1k
                {
560
22.1k
                    {"blockhash", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "If set, the block hash to list transactions since, otherwise list all transactions."},
561
22.1k
                    {"target_confirmations", RPCArg::Type::NUM, RPCArg::Default{1}, "Return the nth block hash from the main chain. e.g. 1 would mean the best block hash. Note: this is not used as a filter, but only affects [lastblock] in the return value"},
562
22.1k
                    {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"},
563
22.1k
                    {"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true}, "Show transactions that were removed due to a reorg in the \"removed\" array\n"
564
22.1k
                                                                       "(not guaranteed to work on pruned nodes)"},
565
22.1k
                    {"include_change", RPCArg::Type::BOOL, RPCArg::Default{false}, "Also add entries for change outputs.\n"},
566
22.1k
                    {"label", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Return only incoming transactions paying to addresses with the specified label.\n"},
567
22.1k
                },
568
22.1k
                RPCResult{
569
22.1k
                    RPCResult::Type::OBJ, "", "",
570
22.1k
                    {
571
22.1k
                        {RPCResult::Type::ARR, "transactions", "",
572
22.1k
                        {
573
22.1k
                            {RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
574
22.1k
                            {
575
22.1k
                                {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction."},
576
22.1k
                                {RPCResult::Type::STR, "address",  /*optional=*/true, "The bitcoin address of the transaction (not returned if the output does not have an address, e.g. OP_RETURN null data)."},
577
22.1k
                                {RPCResult::Type::STR, "category", "The transaction category.\n"
578
22.1k
                                    "\"send\"                  Transactions sent.\n"
579
22.1k
                                    "\"receive\"               Non-coinbase transactions received.\n"
580
22.1k
                                    "\"generate\"              Coinbase transactions received with more than 100 confirmations.\n"
581
22.1k
                                    "\"immature\"              Coinbase transactions received with 100 or fewer confirmations.\n"
582
22.1k
                                    "\"orphan\"                Orphaned coinbase transactions received."},
583
22.1k
                                {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT + ". This is negative for the 'send' category, and is positive\n"
584
22.1k
                                    "for all other categories"},
585
22.1k
                                {RPCResult::Type::NUM, "vout", "the vout value"},
586
22.1k
                                {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n"
587
22.1k
                                     "'send' category of transactions."},
588
22.1k
                            },
589
22.1k
                            TransactionDescriptionString()),
590
22.1k
                            {
591
22.1k
                                {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
592
22.1k
                                {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any"},
593
22.1k
                            })},
594
22.1k
                        }},
595
22.1k
                        {RPCResult::Type::ARR, "removed", /*optional=*/true, "<structure is the same as \"transactions\" above, only present if include_removed=true>\n"
596
22.1k
                            "Note: transactions that were re-added in the active chain will appear as-is in this array, and may thus have a positive confirmation count."
597
22.1k
                        , {{RPCResult::Type::ELISION, "", ""},}},
598
22.1k
                        {RPCResult::Type::STR_HEX, "lastblock", "The hash of the block (target_confirmations-1) from the best block on the main chain, or the genesis hash if the referenced block does not exist yet. This is typically used to feed back into listsinceblock the next time you call it. So you would generally use a target_confirmations of say 6, so you will be continually re-notified of transactions until they've reached 6 confirmations plus any new ones"},
599
22.1k
                    }
600
22.1k
                },
601
22.1k
                RPCExamples{
602
22.1k
                    HelpExampleCli("listsinceblock", "")
603
22.1k
            + HelpExampleCli("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\" 6")
604
22.1k
            + HelpExampleRpc("listsinceblock", "\"000000000000000bacf66f7497b7dc45ef753ee9a7d38571037cdb1a57f663ad\", 6")
605
22.1k
                },
606
22.1k
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
607
22.1k
{
608
0
    const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
609
0
    if (!pwallet) return UniValue::VNULL;
  Branch (609:9): [True: 0, False: 0]
610
611
0
    const CWallet& wallet = *pwallet;
612
    // Make sure the results are valid at least up to the most recent block
613
    // the user could have gotten from another RPC command prior to now
614
0
    wallet.BlockUntilSyncedToCurrentChain();
615
616
0
    LOCK(wallet.cs_wallet);
617
618
0
    std::optional<int> height;    // Height of the specified block or the common ancestor, if the block provided was in a deactivated chain.
619
0
    std::optional<int> altheight; // Height of the specified block, even if it's in a deactivated chain.
620
0
    int target_confirms = 1;
621
0
    isminefilter filter = ISMINE_SPENDABLE;
622
623
0
    uint256 blockId;
624
0
    if (!request.params[0].isNull() && !request.params[0].get_str().empty()) {
  Branch (624:9): [True: 0, False: 0]
  Branch (624:40): [True: 0, False: 0]
625
0
        blockId = ParseHashV(request.params[0], "blockhash");
626
0
        height = int{};
627
0
        altheight = int{};
628
0
        if (!wallet.chain().findCommonAncestor(blockId, wallet.GetLastBlockHash(), /*ancestor_out=*/FoundBlock().height(*height), /*block1_out=*/FoundBlock().height(*altheight))) {
  Branch (628:13): [True: 0, False: 0]
629
0
            throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
630
0
        }
631
0
    }
632
633
0
    if (!request.params[1].isNull()) {
  Branch (633:9): [True: 0, False: 0]
634
0
        target_confirms = request.params[1].getInt<int>();
635
636
0
        if (target_confirms < 1) {
  Branch (636:13): [True: 0, False: 0]
637
0
            throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter");
638
0
        }
639
0
    }
640
641
0
    if (ParseIncludeWatchonly(request.params[2], wallet)) {
  Branch (641:9): [True: 0, False: 0]
642
0
        filter |= ISMINE_WATCH_ONLY;
643
0
    }
644
645
0
    bool include_removed = (request.params[3].isNull() || request.params[3].get_bool());
  Branch (645:29): [True: 0, False: 0]
  Branch (645:59): [True: 0, False: 0]
646
0
    bool include_change = (!request.params[4].isNull() && request.params[4].get_bool());
  Branch (646:28): [True: 0, False: 0]
  Branch (646:59): [True: 0, False: 0]
647
648
    // Only set it if 'label' was provided.
649
0
    std::optional<std::string> filter_label;
650
0
    if (!request.params[5].isNull()) filter_label.emplace(LabelFromValue(request.params[5]));
  Branch (650:9): [True: 0, False: 0]
651
652
0
    int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1;
  Branch (652:17): [True: 0, False: 0]
653
654
0
    UniValue transactions(UniValue::VARR);
655
656
0
    for (const auto& [_, tx] : wallet.mapWallet) {
  Branch (656:30): [True: 0, False: 0]
657
658
0
        if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) {
  Branch (658:13): [True: 0, False: 0]
  Branch (658:28): [True: 0, False: 0]
659
0
            ListTransactions(wallet, tx, 0, true, transactions, filter, filter_label, include_change);
660
0
        }
661
0
    }
662
663
    // when a reorg'd block is requested, we also list any relevant transactions
664
    // in the blocks of the chain that was detached
665
0
    UniValue removed(UniValue::VARR);
666
0
    while (include_removed && altheight && *altheight > *height) {
  Branch (666:12): [True: 0, False: 0]
  Branch (666:31): [True: 0, False: 0]
  Branch (666:44): [True: 0, False: 0]
667
0
        CBlock block;
668
0
        if (!wallet.chain().findBlock(blockId, FoundBlock().data(block)) || block.IsNull()) {
  Branch (668:13): [True: 0, False: 0]
  Branch (668:13): [True: 0, False: 0]
  Branch (668:77): [True: 0, False: 0]
669
0
            throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk");
670
0
        }
671
0
        for (const CTransactionRef& tx : block.vtx) {
  Branch (671:40): [True: 0, False: 0]
672
0
            auto it = wallet.mapWallet.find(tx->GetHash());
673
0
            if (it != wallet.mapWallet.end()) {
  Branch (673:17): [True: 0, False: 0]
674
                // We want all transactions regardless of confirmation count to appear here,
675
                // even negative confirmation ones, hence the big negative.
676
0
                ListTransactions(wallet, it->second, -100000000, true, removed, filter, filter_label, include_change);
677
0
            }
678
0
        }
679
0
        blockId = block.hashPrevBlock;
680
0
        --*altheight;
681
0
    }
682
683
0
    uint256 lastblock;
684
0
    target_confirms = std::min(target_confirms, wallet.GetLastBlockHeight() + 1);
685
0
    CHECK_NONFATAL(wallet.chain().findAncestorByHeight(wallet.GetLastBlockHash(), wallet.GetLastBlockHeight() + 1 - target_confirms, FoundBlock().hash(lastblock)));
686
687
0
    UniValue ret(UniValue::VOBJ);
688
0
    ret.pushKV("transactions", std::move(transactions));
689
0
    if (include_removed) ret.pushKV("removed", std::move(removed));
  Branch (689:9): [True: 0, False: 0]
690
0
    ret.pushKV("lastblock", lastblock.GetHex());
691
692
0
    return ret;
693
0
},
694
22.1k
    };
695
22.1k
}
696
697
RPCHelpMan gettransaction()
698
22.1k
{
699
22.1k
    return RPCHelpMan{
700
22.1k
        "gettransaction",
701
22.1k
        "Get detailed information about in-wallet transaction <txid>\n",
702
22.1k
                {
703
22.1k
                    {"txid", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction id"},
704
22.1k
                    {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"},
705
22.1k
                            "Whether to include watch-only addresses in balance calculation and details[]"},
706
22.1k
                    {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false},
707
22.1k
                            "Whether to include a `decoded` field containing the decoded transaction (equivalent to RPC decoderawtransaction)"},
708
22.1k
                },
709
22.1k
                RPCResult{
710
22.1k
                    RPCResult::Type::OBJ, "", "", Cat(Cat<std::vector<RPCResult>>(
711
22.1k
                    {
712
22.1k
                        {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT},
713
22.1k
                        {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the\n"
714
22.1k
                                     "'send' category of transactions."},
715
22.1k
                    },
716
22.1k
                    TransactionDescriptionString()),
717
22.1k
                    {
718
22.1k
                        {RPCResult::Type::ARR, "details", "",
719
22.1k
                        {
720
22.1k
                            {RPCResult::Type::OBJ, "", "",
721
22.1k
                            {
722
22.1k
                                {RPCResult::Type::BOOL, "involvesWatchonly", /*optional=*/true, "Only returns true if imported addresses were involved in transaction."},
723
22.1k
                                {RPCResult::Type::STR, "address", /*optional=*/true, "The bitcoin address involved in the transaction."},
724
22.1k
                                {RPCResult::Type::STR, "category", "The transaction category.\n"
725
22.1k
                                    "\"send\"                  Transactions sent.\n"
726
22.1k
                                    "\"receive\"               Non-coinbase transactions received.\n"
727
22.1k
                                    "\"generate\"              Coinbase transactions received with more than 100 confirmations.\n"
728
22.1k
                                    "\"immature\"              Coinbase transactions received with 100 or fewer confirmations.\n"
729
22.1k
                                    "\"orphan\"                Orphaned coinbase transactions received."},
730
22.1k
                                {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT},
731
22.1k
                                {RPCResult::Type::STR, "label", /*optional=*/true, "A comment for the address/transaction, if any"},
732
22.1k
                                {RPCResult::Type::NUM, "vout", "the vout value"},
733
22.1k
                                {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the \n"
734
22.1k
                                    "'send' category of transactions."},
735
22.1k
                                {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."},
736
22.1k
                                {RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the output script of this coin.", {
737
22.1k
                                    {RPCResult::Type::STR, "desc", "The descriptor string."},
738
22.1k
                                }},
739
22.1k
                            }},
740
22.1k
                        }},
741
22.1k
                        {RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"},
742
22.1k
                        {RPCResult::Type::OBJ, "decoded", /*optional=*/true, "The decoded transaction (only present when `verbose` is passed)",
743
22.1k
                        {
744
22.1k
                            {RPCResult::Type::ELISION, "", "Equivalent to the RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed."},
745
22.1k
                        }},
746
22.1k
                        RESULT_LAST_PROCESSED_BLOCK,
747
22.1k
                    })
748
22.1k
                },
749
22.1k
                RPCExamples{
750
22.1k
                    HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
751
22.1k
            + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" true")
752
22.1k
            + HelpExampleCli("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\" false true")
753
22.1k
            + HelpExampleRpc("gettransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
754
22.1k
                },
755
22.1k
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
756
22.1k
{
757
0
    const std::shared_ptr<const CWallet> pwallet = GetWalletForJSONRPCRequest(request);
758
0
    if (!pwallet) return UniValue::VNULL;
  Branch (758:9): [True: 0, False: 0]
759
760
    // Make sure the results are valid at least up to the most recent block
761
    // the user could have gotten from another RPC command prior to now
762
0
    pwallet->BlockUntilSyncedToCurrentChain();
763
764
0
    LOCK(pwallet->cs_wallet);
765
766
0
    Txid hash{Txid::FromUint256(ParseHashV(request.params[0], "txid"))};
767
768
0
    isminefilter filter = ISMINE_SPENDABLE;
769
770
0
    if (ParseIncludeWatchonly(request.params[1], *pwallet)) {
  Branch (770:9): [True: 0, False: 0]
771
0
        filter |= ISMINE_WATCH_ONLY;
772
0
    }
773
774
0
    bool verbose = request.params[2].isNull() ? false : request.params[2].get_bool();
  Branch (774:20): [True: 0, False: 0]
775
776
0
    UniValue entry(UniValue::VOBJ);
777
0
    auto it = pwallet->mapWallet.find(hash);
778
0
    if (it == pwallet->mapWallet.end()) {
  Branch (778:9): [True: 0, False: 0]
779
0
        throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
780
0
    }
781
0
    const CWalletTx& wtx = it->second;
782
783
0
    CAmount nCredit = CachedTxGetCredit(*pwallet, wtx, filter);
784
0
    CAmount nDebit = CachedTxGetDebit(*pwallet, wtx, filter);
785
0
    CAmount nNet = nCredit - nDebit;
786
0
    CAmount nFee = (CachedTxIsFromMe(*pwallet, wtx, filter) ? wtx.tx->GetValueOut() - nDebit : 0);
  Branch (786:21): [True: 0, False: 0]
787
788
0
    entry.pushKV("amount", ValueFromAmount(nNet - nFee));
789
0
    if (CachedTxIsFromMe(*pwallet, wtx, filter))
  Branch (789:9): [True: 0, False: 0]
790
0
        entry.pushKV("fee", ValueFromAmount(nFee));
791
792
0
    WalletTxToJSON(*pwallet, wtx, entry);
793
794
0
    UniValue details(UniValue::VARR);
795
0
    ListTransactions(*pwallet, wtx, 0, false, details, filter, /*filter_label=*/std::nullopt);
796
0
    entry.pushKV("details", std::move(details));
797
798
0
    entry.pushKV("hex", EncodeHexTx(*wtx.tx));
799
800
0
    if (verbose) {
  Branch (800:9): [True: 0, False: 0]
801
0
        UniValue decoded(UniValue::VOBJ);
802
0
        TxToUniv(*wtx.tx, /*block_hash=*/uint256(), /*entry=*/decoded, /*include_hex=*/false);
803
0
        entry.pushKV("decoded", std::move(decoded));
804
0
    }
805
806
0
    AppendLastProcessedBlock(entry, *pwallet);
807
0
    return entry;
808
0
},
809
22.1k
    };
810
22.1k
}
811
812
RPCHelpMan abandontransaction()
813
22.1k
{
814
22.1k
    return RPCHelpMan{
815
22.1k
        "abandontransaction",
816
22.1k
        "Mark in-wallet transaction <txid> as abandoned\n"
817
22.1k
                "This will mark this transaction and all its in-wallet descendants as abandoned which will allow\n"
818
22.1k
                "for their inputs to be respent.  It can be used to replace \"stuck\" or evicted transactions.\n"
819
22.1k
                "It only works on transactions which are not included in a block and are not currently in the mempool.\n"
820
22.1k
                "It has no effect on transactions which are already abandoned.\n",
821
22.1k
                {
822
22.1k
                    {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
823
22.1k
                },
824
22.1k
                RPCResult{RPCResult::Type::NONE, "", ""},
825
22.1k
                RPCExamples{
826
22.1k
                    HelpExampleCli("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
827
22.1k
            + HelpExampleRpc("abandontransaction", "\"1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d\"")
828
22.1k
                },
829
22.1k
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
830
22.1k
{
831
0
    std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
832
0
    if (!pwallet) return UniValue::VNULL;
  Branch (832:9): [True: 0, False: 0]
833
834
    // Make sure the results are valid at least up to the most recent block
835
    // the user could have gotten from another RPC command prior to now
836
0
    pwallet->BlockUntilSyncedToCurrentChain();
837
838
0
    LOCK(pwallet->cs_wallet);
839
840
0
    Txid hash{Txid::FromUint256(ParseHashV(request.params[0], "txid"))};
841
842
0
    if (!pwallet->mapWallet.count(hash)) {
  Branch (842:9): [True: 0, False: 0]
843
0
        throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id");
844
0
    }
845
0
    if (!pwallet->AbandonTransaction(hash)) {
  Branch (845:9): [True: 0, False: 0]
846
0
        throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not eligible for abandonment");
847
0
    }
848
849
0
    return UniValue::VNULL;
850
0
},
851
22.1k
    };
852
22.1k
}
853
854
RPCHelpMan rescanblockchain()
855
22.1k
{
856
22.1k
    return RPCHelpMan{
857
22.1k
        "rescanblockchain",
858
22.1k
        "Rescan the local blockchain for wallet related transactions.\n"
859
22.1k
                "Note: Use \"getwalletinfo\" to query the scanning progress.\n"
860
22.1k
                "The rescan is significantly faster when used on a descriptor wallet\n"
861
22.1k
                "and block filters are available (using startup option \"-blockfilterindex=1\").\n",
862
22.1k
                {
863
22.1k
                    {"start_height", RPCArg::Type::NUM, RPCArg::Default{0}, "block height where the rescan should start"},
864
22.1k
                    {"stop_height", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "the last block height that should be scanned. If none is provided it will rescan up to the tip at return time of this call."},
865
22.1k
                },
866
22.1k
                RPCResult{
867
22.1k
                    RPCResult::Type::OBJ, "", "",
868
22.1k
                    {
869
22.1k
                        {RPCResult::Type::NUM, "start_height", "The block height where the rescan started (the requested height or 0)"},
870
22.1k
                        {RPCResult::Type::NUM, "stop_height", "The height of the last rescanned block. May be null in rare cases if there was a reorg and the call didn't scan any blocks because they were already scanned in the background."},
871
22.1k
                    }
872
22.1k
                },
873
22.1k
                RPCExamples{
874
22.1k
                    HelpExampleCli("rescanblockchain", "100000 120000")
875
22.1k
            + HelpExampleRpc("rescanblockchain", "100000, 120000")
876
22.1k
                },
877
22.1k
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
878
22.1k
{
879
0
    std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
880
0
    if (!pwallet) return UniValue::VNULL;
  Branch (880:9): [True: 0, False: 0]
881
0
    CWallet& wallet{*pwallet};
882
883
    // Make sure the results are valid at least up to the most recent block
884
    // the user could have gotten from another RPC command prior to now
885
0
    wallet.BlockUntilSyncedToCurrentChain();
886
887
0
    WalletRescanReserver reserver(*pwallet);
888
0
    if (!reserver.reserve(/*with_passphrase=*/true)) {
  Branch (888:9): [True: 0, False: 0]
889
0
        throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
890
0
    }
891
892
0
    int start_height = 0;
893
0
    std::optional<int> stop_height;
894
0
    uint256 start_block;
895
896
0
    LOCK(pwallet->m_relock_mutex);
897
0
    {
898
0
        LOCK(pwallet->cs_wallet);
899
0
        EnsureWalletIsUnlocked(*pwallet);
900
0
        int tip_height = pwallet->GetLastBlockHeight();
901
902
0
        if (!request.params[0].isNull()) {
  Branch (902:13): [True: 0, False: 0]
903
0
            start_height = request.params[0].getInt<int>();
904
0
            if (start_height < 0 || start_height > tip_height) {
  Branch (904:17): [True: 0, False: 0]
  Branch (904:37): [True: 0, False: 0]
905
0
                throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid start_height");
906
0
            }
907
0
        }
908
909
0
        if (!request.params[1].isNull()) {
  Branch (909:13): [True: 0, False: 0]
910
0
            stop_height = request.params[1].getInt<int>();
911
0
            if (*stop_height < 0 || *stop_height > tip_height) {
  Branch (911:17): [True: 0, False: 0]
  Branch (911:37): [True: 0, False: 0]
912
0
                throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid stop_height");
913
0
            } else if (*stop_height < start_height) {
  Branch (913:24): [True: 0, False: 0]
914
0
                throw JSONRPCError(RPC_INVALID_PARAMETER, "stop_height must be greater than start_height");
915
0
            }
916
0
        }
917
918
        // We can't rescan unavailable blocks, stop and throw an error
919
0
        if (!pwallet->chain().hasBlocks(pwallet->GetLastBlockHash(), start_height, stop_height)) {
  Branch (919:13): [True: 0, False: 0]
920
0
            if (pwallet->chain().havePruned() && pwallet->chain().getPruneHeight() >= start_height) {
  Branch (920:17): [True: 0, False: 0]
  Branch (920:17): [True: 0, False: 0]
  Branch (920:50): [True: 0, False: 0]
921
0
                throw JSONRPCError(RPC_MISC_ERROR, "Can't rescan beyond pruned data. Use RPC call getblockchaininfo to determine your pruned height.");
922
0
            }
923
0
            if (pwallet->chain().hasAssumedValidChain()) {
  Branch (923:17): [True: 0, False: 0]
924
0
                throw JSONRPCError(RPC_MISC_ERROR, "Failed to rescan unavailable blocks likely due to an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later.");
925
0
            }
926
0
            throw JSONRPCError(RPC_MISC_ERROR, "Failed to rescan unavailable blocks, potentially caused by data corruption. If the issue persists you may want to reindex (see -reindex option).");
927
0
        }
928
929
0
        CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(pwallet->GetLastBlockHash(), start_height, FoundBlock().hash(start_block)));
930
0
    }
931
932
0
    CWallet::ScanResult result =
933
0
        pwallet->ScanForWalletTransactions(start_block, start_height, stop_height, reserver, /*fUpdate=*/true, /*save_progress=*/false);
934
0
    switch (result.status) {
  Branch (934:13): [True: 0, False: 0]
935
0
    case CWallet::ScanResult::SUCCESS:
  Branch (935:5): [True: 0, False: 0]
936
0
        break;
937
0
    case CWallet::ScanResult::FAILURE:
  Branch (937:5): [True: 0, False: 0]
938
0
        throw JSONRPCError(RPC_MISC_ERROR, "Rescan failed. Potentially corrupted data files.");
939
0
    case CWallet::ScanResult::USER_ABORT:
  Branch (939:5): [True: 0, False: 0]
940
0
        throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted.");
941
        // no default case, so the compiler can warn about missing cases
942
0
    }
943
0
    UniValue response(UniValue::VOBJ);
944
0
    response.pushKV("start_height", start_height);
945
0
    response.pushKV("stop_height", result.last_scanned_height ? *result.last_scanned_height : UniValue());
  Branch (945:36): [True: 0, False: 0]
946
0
    return response;
947
0
},
948
22.1k
    };
949
22.1k
}
950
951
RPCHelpMan abortrescan()
952
22.1k
{
953
22.1k
    return RPCHelpMan{"abortrescan",
954
22.1k
                "Stops current wallet rescan triggered by an RPC call, e.g. by a rescanblockchain call.\n"
955
22.1k
                "Note: Use \"getwalletinfo\" to query the scanning progress.\n",
956
22.1k
                {},
957
22.1k
                RPCResult{RPCResult::Type::BOOL, "", "Whether the abort was successful"},
958
22.1k
                RPCExamples{
959
22.1k
            "\nImport a private key\n"
960
22.1k
            + HelpExampleCli("rescanblockchain", "") +
961
22.1k
            "\nAbort the running wallet rescan\n"
962
22.1k
            + HelpExampleCli("abortrescan", "") +
963
22.1k
            "\nAs a JSON-RPC call\n"
964
22.1k
            + HelpExampleRpc("abortrescan", "")
965
22.1k
                },
966
22.1k
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
967
22.1k
{
968
0
    std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
969
0
    if (!pwallet) return UniValue::VNULL;
  Branch (969:9): [True: 0, False: 0]
970
971
0
    if (!pwallet->IsScanning() || pwallet->IsAbortingRescan()) return false;
  Branch (971:9): [True: 0, False: 0]
  Branch (971:35): [True: 0, False: 0]
972
0
    pwallet->AbortRescan();
973
0
    return true;
974
0
},
975
22.1k
    };
976
22.1k
}
977
} // namespace wallet