Coverage Report

Created: 2024-09-19 18:47

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