diff --git a/doc/release-notes-25122.md b/doc/release-notes-25122.md new file mode 100644 index 000000000000..5c5f0f911154 --- /dev/null +++ b/doc/release-notes-25122.md @@ -0,0 +1,7 @@ + +Wallet +------ + +- RPC `getreceivedbylabel` now returns an error, "Label not found + in wallet" (-4), if the label is not in the address book. (dash#7550) + diff --git a/doc/release-notes-25504.md b/doc/release-notes-25504.md new file mode 100644 index 000000000000..bf80f180318a --- /dev/null +++ b/doc/release-notes-25504.md @@ -0,0 +1,6 @@ +Updated RPCs +------------ + +- The `listsinceblock`, `listtransactions` and `gettransaction` output now contain a new + `parent_descs` field for every "receive" entry. +- A new optional `include_change` parameter was added to the `listsinceblock` command. diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 427909b8e12f..42486408b59d 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -235,7 +235,8 @@ BITCOIN_TESTS += \ wallet/test/init_tests.cpp \ wallet/test/ismine_tests.cpp \ wallet/test/rpc_util_tests.cpp \ - wallet/test/scriptpubkeyman_tests.cpp + wallet/test/scriptpubkeyman_tests.cpp \ + wallet/test/walletload_tests.cpp FUZZ_SUITE_LD_COMMON +=\ $(SQLITE_LIBS) \ diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 2579ed364274..972e99c94924 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -95,6 +95,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "listsinceblock", 1, "target_confirmations" }, { "listsinceblock", 2, "include_watchonly" }, { "listsinceblock", 3, "include_removed" }, + { "listsinceblock", 4, "include_change" }, { "sendmany", 1, "amounts" }, { "sendmany", 2, "minconf" }, { "sendmany", 3, "addlocked" }, diff --git a/src/wallet/coinselection.cpp b/src/wallet/coinselection.cpp index eff3a31e8322..28c73863dff9 100644 --- a/src/wallet/coinselection.cpp +++ b/src/wallet/coinselection.cpp @@ -190,7 +190,7 @@ util::Result SelectCoinsBnB(std::vector& utxo_pool for (const size_t& i : best_selection) { result.AddInput(utxo_pool.at(i)); } - result.ComputeAndSetWaste(CAmount{0}); + result.ComputeAndSetWaste(cost_of_change, cost_of_change, CAmount{0}); assert(best_waste == result.GetWaste()); return result; @@ -206,13 +206,21 @@ class MinOutputGroupComparator }; util::Result SelectCoinsSRD(const std::vector& utxo_pool, CAmount target_value, FastRandomContext& rng, - int max_weight) + int max_weight, bool only_fully_mixed) { if (utxo_pool.empty()) return util::Error(); SelectionResult result(target_value, SelectionAlgorithm::SRD); std::priority_queue, MinOutputGroupComparator> heap; + // Include change for SRD as we want to avoid making really small change if the selection just + // barely meets the target. Just use the lower bound change target instead of the randomly + // generated one, since SRD will result in a random change amount anyway; avoid making the + // target needlessly large. + if (!only_fully_mixed) { + target_value += CHANGE_LOWER; + } + std::vector indexes; indexes.resize(utxo_pool.size()); std::iota(indexes.begin(), indexes.end(), 0); @@ -530,20 +538,26 @@ CAmount GetSelectionWaste(const std::set& inputs, CAmount change_cost, return waste; } -CAmount GenerateChangeTarget(CAmount payment_value, FastRandomContext& rng) +CAmount GenerateChangeTarget(const CAmount payment_value, const CAmount change_fee, FastRandomContext& rng) { if (payment_value <= CHANGE_LOWER / 2) { - return CHANGE_LOWER; + return change_fee + CHANGE_LOWER; } else { // random value between 50ksat and min (payment_value * 2, 1milsat) const auto upper_bound = std::min(payment_value * 2, CHANGE_UPPER); - return rng.randrange(upper_bound - CHANGE_LOWER) + CHANGE_LOWER; + return change_fee + rng.randrange(upper_bound - CHANGE_LOWER) + CHANGE_LOWER; } } -void SelectionResult::ComputeAndSetWaste(CAmount change_cost) +void SelectionResult::ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee) { - m_waste = GetSelectionWaste(m_selected_inputs, change_cost, m_target, m_use_effective); + const CAmount change = GetChange(min_viable_change, change_fee); + + if (change > 0) { + m_waste = GetSelectionWaste(m_selected_inputs, change_cost, m_target, m_use_effective); + } else { + m_waste = GetSelectionWaste(m_selected_inputs, 0, m_target, m_use_effective); + } } CAmount SelectionResult::GetWaste() const @@ -556,6 +570,11 @@ CAmount SelectionResult::GetSelectedValue() const return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.txout.nValue; }); } +CAmount SelectionResult::GetSelectedEffectiveValue() const +{ + return std::accumulate(m_selected_inputs.cbegin(), m_selected_inputs.cend(), CAmount{0}, [](CAmount sum, const auto& coin) { return sum + coin.GetEffectiveValue(); }); +} + void SelectionResult::Clear() { m_selected_inputs.clear(); @@ -568,6 +587,16 @@ void SelectionResult::AddInput(const OutputGroup& group) m_use_effective = !group.m_subtract_fee_outputs; } +void SelectionResult::Merge(const SelectionResult& other) +{ + m_target += other.m_target; + m_use_effective |= other.m_use_effective; + if (m_algo == SelectionAlgorithm::MANUAL) { + m_algo = other.m_algo; + } + util::insert(m_selected_inputs, other.m_selected_inputs); +} + const std::set& SelectionResult::GetInputSet() const { return m_selected_inputs; @@ -605,4 +634,24 @@ std::string GetAlgorithmName(const SelectionAlgorithm algo) } assert(false); } + +CAmount SelectionResult::GetChange(const CAmount min_viable_change, const CAmount change_fee) const +{ + // change = SUM(inputs) - SUM(outputs) - fees + // 1) With SFFO we don't pay any fees + // 2) Otherwise we pay all the fees: + // - input fees are covered by GetSelectedEffectiveValue() + // - non_input_fee is included in m_target + // - change_fee + const CAmount change = m_use_effective + ? GetSelectedEffectiveValue() - m_target - change_fee + : GetSelectedValue() - m_target; + + if (change < min_viable_change) { + return 0; + } + + return change; +} + } // namespace wallet diff --git a/src/wallet/coinselection.h b/src/wallet/coinselection.h index 7d3456297054..2d97331942b4 100644 --- a/src/wallet/coinselection.h +++ b/src/wallet/coinselection.h @@ -124,6 +124,10 @@ struct CoinSelectionParams { /** Mininmum change to target in Knapsack solver: select coins to cover the payment and * at least this value of change. */ CAmount m_min_change_target{0}; + /** Minimum amount for creating a change output. + * If change budget is smaller than min_change then we forgo creation of change output. + */ + CAmount min_viable_change{0}; /** Cost of creating the change output. */ CAmount m_change_fee{0}; /** Cost of creating the change output + cost of spending the change output in the future. */ @@ -251,6 +255,7 @@ struct OutputGroup /** Choose a random change target for each transaction to make it harder to fingerprint the Core * wallet based on the change output values of transactions it creates. + * Change target covers at least change fees and adds a random value on top of it. * The random value is between 50ksat and min(2 * payment_value, 1milsat) * When payment_value <= 25ksat, the value is just 50ksat. * @@ -260,8 +265,9 @@ struct OutputGroup * coins selected are just sufficient to cover the payment amount ("unnecessary input" heuristic). * * @param[in] payment_value Average payment value of the transaction output(s). + * @param[in] change_fee Fee for creating a change output. */ -[[nodiscard]] CAmount GenerateChangeTarget(CAmount payment_value, FastRandomContext& rng); +[[nodiscard]] CAmount GenerateChangeTarget(const CAmount payment_value, const CAmount change_fee, FastRandomContext& rng); enum class SelectionAlgorithm : uint8_t { @@ -278,17 +284,16 @@ struct SelectionResult private: /** Set of inputs selected by the algorithm to use in the transaction */ std::set m_selected_inputs; + /** The target the algorithm selected for. Equal to the recipient amount plus non-input fees */ + CAmount m_target; + /** The algorithm used to produce this result */ + SelectionAlgorithm m_algo; /** Whether the input values for calculations should be the effective value (true) or normal value (false) */ bool m_use_effective{false}; /** The computed waste */ std::optional m_waste; public: - /** The target the algorithm selected for. Note that this may not be equal to the recipient amount as it can include non-input fees */ - const CAmount m_target; - /** The algorithm used to produce this result */ - const SelectionAlgorithm m_algo; - explicit SelectionResult(const CAmount target, SelectionAlgorithm algo) : m_target(target), m_algo(algo) {} @@ -297,20 +302,47 @@ struct SelectionResult /** Get the sum of the input values */ [[nodiscard]] CAmount GetSelectedValue() const; + [[nodiscard]] CAmount GetSelectedEffectiveValue() const; + void Clear(); void AddInput(const OutputGroup& group); /** Calculates and stores the waste for this selection via GetSelectionWaste */ - void ComputeAndSetWaste(CAmount change_cost); + void ComputeAndSetWaste(const CAmount min_viable_change, const CAmount change_cost, const CAmount change_fee); [[nodiscard]] CAmount GetWaste() const; + void Merge(const SelectionResult& other); + /** Get m_selected_inputs */ const std::set& GetInputSet() const; /** Get the vector of COutputs that will be used to fill in a CTransaction's vin */ std::vector GetShuffledInputVector() const; bool operator<(SelectionResult other) const; + + /** Get the amount for the change output after paying needed fees. + * + * The change amount is not 100% precise due to discrepancies in fee calculation. + * The final change amount (if any) should be corrected after calculating the final tx fees. + * When there is a discrepancy, most of the time the final change would be slightly bigger than estimated. + * + * Following are the possible factors of discrepancy: + * + non-input fees always include segwit flags + * + input fee estimation always include segwit stack size + * + input fees are rounded individually and not collectively, which leads to small rounding errors + * - input counter size is always assumed to be 1vbyte + * + * @param[in] min_viable_change Minimum amount for change output, if change would be less then we forgo change + * @param[in] change_fee Fees to include change output in the tx + * @returns Amount for change output, 0 when there is no change. + * + */ + CAmount GetChange(const CAmount min_viable_change, const CAmount change_fee) const; + + CAmount GetTarget() const { return m_target; } + + SelectionAlgorithm GetAlgo() const { return m_algo; } }; util::Result SelectCoinsBnB(std::vector& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change, @@ -326,7 +358,7 @@ util::Result SelectCoinsBnB(std::vector& utxo_pool * @returns If successful, a valid SelectionResult, otherwise, util::Error */ util::Result SelectCoinsSRD(const std::vector& utxo_pool, CAmount target_value, FastRandomContext& rng, - int max_weight); + int max_weight, bool only_fully_mixed); // Original coin selection algorithm as a fallback util::Result KnapsackSolver(std::vector& groups, const CAmount& nTargetValue, diff --git a/src/wallet/receive.cpp b/src/wallet/receive.cpp index 43bf3b633529..309c43450a0f 100644 --- a/src/wallet/receive.cpp +++ b/src/wallet/receive.cpp @@ -194,7 +194,8 @@ CAmount CachedTxGetAvailableCredit(const CWallet& wallet, const CWalletTx& wtx, void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx, std::list& listReceived, - std::list& listSent, CAmount& nFee, const isminefilter& filter) + std::list& listSent, CAmount& nFee, const isminefilter& filter, + bool include_change) { nFee = 0; listReceived.clear(); @@ -219,8 +220,7 @@ void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx, // 2) the output is to us (received) if (nDebit > 0) { - // Don't report 'change' txouts - if (OutputIsChange(wallet, txout)) + if (!include_change && OutputIsChange(wallet, txout)) continue; } else if (!(fIsMine & filter)) diff --git a/src/wallet/receive.h b/src/wallet/receive.h index f19901794660..0acc4c6373e7 100644 --- a/src/wallet/receive.h +++ b/src/wallet/receive.h @@ -42,7 +42,8 @@ struct COutputEntry void CachedTxGetAmounts(const CWallet& wallet, const CWalletTx& wtx, std::list& listReceived, std::list& listSent, - CAmount& nFee, const isminefilter& filter); + CAmount& nFee, const isminefilter& filter, + bool include_change); bool CachedTxIsFromMe(const CWallet& wallet, const CWalletTx& wtx, const isminefilter& filter); bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx, std::set& trusted_parents) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx); diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index ce9e6b1dc3da..c0bd143be002 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -92,7 +92,7 @@ RPCHelpMan importprivkey() "\nNote: This call can take over an hour to complete if rescan is true, during that time, other rpc calls\n" "may report that the imported key exists but related transactions are still missing, leading to temporarily incorrect/bogus balances and unspent outputs until rescan completes.\n" "The rescan parameter can be set to false if the key was never used to create transactions. If it is set to false,\n" - "but the key was used to create transactions, rescanwallet needs to be called with the appropriate block range.\n" + "but the key was used to create transactions, rescanblockchain needs to be called with the appropriate block range.\n" "Note: This command is only compatible with legacy wallets. Use \"importdescriptors\" with \"combo(X)\" for descriptor wallets.\n", { {"privkey", RPCArg::Type::STR, RPCArg::Optional::NO, "The private key (see dumpprivkey)"}, @@ -211,7 +211,7 @@ RPCHelpMan importaddress() "\nNote: This call can take over an hour to complete if rescan is true, during that time, other rpc calls\n" "may report that the imported address exists but related transactions are still missing, leading to temporarily incorrect/bogus balances and unspent outputs until rescan completes.\n" "The rescan parameter can be set to false if the key was never used to create transactions. If it is set to false,\n" - "but the key was used to create transactions, rescanwallet needs to be called with the appropriate block range.\n" + "but the key was used to create transactions, rescanblockchain needs to be called with the appropriate block range.\n" "If you have the full public key, you should call importpubkey instead of this.\n" "Hint: use importmulti to import more than one address.\n" "\nNote: If you import a non-standard raw script in hex form, outputs sending to it will be treated\n" @@ -296,10 +296,7 @@ RPCHelpMan importaddress() if (fRescan) { RescanWallet(*pwallet, reserver); - { - LOCK(pwallet->cs_wallet); - pwallet->ReacceptWalletTransactions(); - } + pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true); } return UniValue::VNULL; @@ -409,7 +406,7 @@ RPCHelpMan importpubkey() "\nNote: This call can take over an hour to complete if rescan is true, during that time, other rpc calls\n" "may report that the imported pubkey exists but related transactions are still missing, leading to temporarily incorrect/bogus balances and unspent outputs until rescan completes.\n" "The rescan parameter can be set to false if the key was never used to create transactions. If it is set to false,\n" - "but the key was used to create transactions, rescanwallet needs to be called with the appropriate block range.\n" + "but the key was used to create transactions, rescanblockchain needs to be called with the appropriate block range.\n" "Note: This command is only compatible with legacy wallets. Use \"importdescriptors\" with \"combo(X)\" for descriptor wallets.\n", { {"pubkey", RPCArg::Type::STR, RPCArg::Optional::NO, "The hex-encoded public key"}, @@ -475,10 +472,7 @@ RPCHelpMan importpubkey() if (fRescan) { RescanWallet(*pwallet, reserver); - { - LOCK(pwallet->cs_wallet); - pwallet->ReacceptWalletTransactions(); - } + pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true); } return UniValue::VNULL; @@ -1490,7 +1484,7 @@ RPCHelpMan importmulti() "\nNote: This call can take over an hour to complete if rescan is true, during that time, other rpc calls\n" "may report that the imported keys, addresses or scripts exists but related transactions are still missing.\n" "The rescan parameter can be set to false if the key was never used to create transactions. If it is set to false,\n" - "but the key was used to create transactions, rescanwallet needs to be called with the appropriate block range.\n" + "but the key was used to create transactions, rescanblockchain needs to be called with the appropriate block range.\n" "Note: This command is only compatible with legacy wallets. Use \"importdescriptors\" for descriptor wallets.\n", { {"requests", RPCArg::Type::ARR, RPCArg::Optional::NO, "Data to be imported", @@ -1638,10 +1632,7 @@ RPCHelpMan importmulti() } if (fRescan && fRunScan && requests.size()) { int64_t scannedTime = pwallet->RescanFromTime(nLowestTimestamp, reserver, true /* update */); - { - LOCK(pwallet->cs_wallet); - pwallet->ReacceptWalletTransactions(); - } + pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true); if (pwallet->IsAbortingRescan()) { throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user."); @@ -1933,10 +1924,7 @@ RPCHelpMan importdescriptors() { // Rescan the blockchain using the lowest timestamp if (rescan) { int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver, true /* update */); - { - LOCK(pwallet->cs_wallet); - pwallet->ReacceptWalletTransactions(); - } + pwallet->ResubmitWalletTransactions(/*relay=*/false, /*force=*/true); if (pwallet->IsAbortingRescan()) { throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user."); diff --git a/src/wallet/rpc/coins.cpp b/src/wallet/rpc/coins.cpp index 845d8003230c..5a060111fb3a 100644 --- a/src/wallet/rpc/coins.cpp +++ b/src/wallet/rpc/coins.cpp @@ -17,28 +17,32 @@ namespace wallet { static CAmount GetReceived(const CWallet& wallet, const UniValue& params, bool by_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) { - std::vector output_scripts; + std::vector addresses; if (by_label) { // Get the set of addresses assigned to label - std::string label = LabelFromValue(params[0]); - for (const auto& address : wallet.ListAddrBookAddresses(CWallet::AddrBookFilter{label})) { - auto output_script{GetScriptForDestination(address)}; - if (wallet.IsMine(output_script)) { - output_scripts.emplace_back(output_script); - } - } + addresses = wallet.ListAddrBookAddresses(CWallet::AddrBookFilter{LabelFromValue(params[0])}); + if (addresses.empty()) throw JSONRPCError(RPC_WALLET_ERROR, "Label not found in wallet"); } else { // Get the address CTxDestination dest = DecodeDestination(params[0].get_str()); if (!IsValidDestination(dest)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Dash address"); } - CScript script_pub_key = GetScriptForDestination(dest); - if (!wallet.IsMine(script_pub_key)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Address not found in wallet"); + addresses.emplace_back(dest); + } + + // Filter by own scripts only + std::vector output_scripts; + for (const auto& address : addresses) { + auto output_script{GetScriptForDestination(address)}; + if (wallet.IsMine(output_script)) { + output_scripts.emplace_back(output_script); } - output_scripts.emplace_back(script_pub_key); + } + + if (output_scripts.empty()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Address not found in wallet"); } // Minimum confirmations @@ -588,6 +592,9 @@ RPCHelpMan listunspent() {RPCResult::Type::BOOL, "solvable", "Whether we know how to spend this output, ignoring the lack of keys"}, {RPCResult::Type::STR, "desc", /*optional=*/true, "(only when solvable) A descriptor for spending this output"}, {RPCResult::Type::BOOL, "reused", /* optional*/ true, "(only present if avoid_reuse is set) Whether this output is reused/dirty (sent to an address that was previously spent from)"}, + {RPCResult::Type::ARR, "parent_descs", /*optional=*/false, "List of parent descriptors for the scriptPubKey of this coin.", { + {RPCResult::Type::STR, "desc", "The descriptor string."}, + }}, {RPCResult::Type::BOOL, "safe", "Whether this output is considered safe to spend. Unconfirmed transactions" "from outside keys and unconfirmed replacement transactions are considered unsafe\n" "and are not eligible for spending by fundrawtransaction and sendtoaddress."}, @@ -758,6 +765,7 @@ RPCHelpMan listunspent() entry.pushKV("desc", descriptor->ToString()); } } + PushParentDescriptors(*pwallet, scriptPubKey, entry); if (avoid_reuse) entry.pushKV("reused", reused); entry.pushKV("safe", out.safe); entry.pushKV("coinjoin_rounds", pwallet->GetRealOutpointCoinJoinRounds(out.outpoint)); diff --git a/src/wallet/rpc/transactions.cpp b/src/wallet/rpc/transactions.cpp index 5aec8db611de..a61780891a19 100644 --- a/src/wallet/rpc/transactions.cpp +++ b/src/wallet/rpc/transactions.cpp @@ -324,13 +324,16 @@ static void MaybePushAddress(UniValue & entry, const CTxDestination &dest) * @param filter_label Optional label string to filter incoming transactions. */ template -static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong, Vec& ret, const isminefilter& filter_ismine, const std::string* filter_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) +static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong, + Vec& ret, const isminefilter& filter_ismine, const std::string* filter_label, + bool include_change = false) + EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) { CAmount nFee; std::list listReceived; std::list listSent; - CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine); + CachedTxGetAmounts(wallet, wtx, listReceived, listSent, nFee, filter_ismine, include_change); bool involvesWatchonly = CachedTxIsFromMe(wallet, wtx, ISMINE_WATCH_ONLY); @@ -377,6 +380,7 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM entry.pushKV("involvesWatchonly", true); } MaybePushAddress(entry, r.destination); + PushParentDescriptors(wallet, wtx.tx->vout.at(r.vout).scriptPubKey, entry); if (wtx.IsCoinBase()) { if (wallet.GetTxDepthInMainChain(wtx) < 1) @@ -435,7 +439,10 @@ static std::vector TransactionDescriptionString() "for 'send' and 'receive' category of transactions."}, {RPCResult::Type::STR, "DS", /*optional=*/true, "Set to \"1\" if the transaction is a CoinJoin send."}, {RPCResult::Type::STR, "comment", /*optional=*/true, "If a comment is associated with the transaction."}, - {RPCResult::Type::STR, "to", /*optional=*/true, "If a comment to is associated with the transaction."}}; + {RPCResult::Type::STR, "to", /*optional=*/true, "If a comment to is associated with the transaction."}, + {RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the scriptPubKey of this coin.", { + {RPCResult::Type::STR, "desc", "The descriptor string."}, + }}}; } RPCHelpMan listtransactions() @@ -562,6 +569,7 @@ RPCHelpMan listsinceblock() {"include_watchonly", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true for watch-only wallets, otherwise false"}, "Include transactions to watch-only addresses (see 'importaddress')"}, {"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true}, "Show transactions that were removed due to a reorg in the \"removed\" array\n" "(not guaranteed to work on pruned nodes)"}, + {"include_change", RPCArg::Type::BOOL, RPCArg::Default{false}, "Also add entries for change outputs.\n"}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -642,6 +650,7 @@ RPCHelpMan listsinceblock() } bool include_removed = (request.params[3].isNull() || request.params[3].get_bool()); + bool include_change = (!request.params[4].isNull() && request.params[4].get_bool()); int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1; @@ -651,7 +660,7 @@ RPCHelpMan listsinceblock() const CWalletTx& tx = pairWtx.second; if (depth == -1 || abs(wallet.GetTxDepthInMainChain(tx)) < depth) { - ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */); + ListTransactions(wallet, tx, 0, true, transactions, filter, nullptr /* filter_label */, /*include_change=*/include_change); } } @@ -668,7 +677,7 @@ RPCHelpMan listsinceblock() if (it != wallet.mapWallet.end()) { // We want all transactions regardless of confirmation count to appear here, // even negative confirmation ones, hence the big negative. - ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */); + ListTransactions(wallet, it->second, -100000000, true, removed, filter, nullptr /* filter_label */, /*include_change=*/include_change); } } blockId = block.hashPrevBlock; @@ -727,6 +736,9 @@ RPCHelpMan gettransaction() {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The amount of the fee in " + CURRENCY_UNIT + ". This is negative and only available for the \n" "'send' category of transactions."}, {RPCResult::Type::BOOL, "abandoned", "'true' if the transaction has been abandoned (inputs are respendable)."}, + {RPCResult::Type::ARR, "parent_descs", /*optional=*/true, "Only if 'category' is 'received'. List of parent descriptors for the scriptPubKey of this coin.", { + {RPCResult::Type::STR, "desc", "The descriptor string."}, + }}, }}, }}, {RPCResult::Type::STR_HEX, "hex", "Raw data for transaction"}, diff --git a/src/wallet/rpc/util.cpp b/src/wallet/rpc/util.cpp index e67be931af6d..25151961df8b 100644 --- a/src/wallet/rpc/util.cpp +++ b/src/wallet/rpc/util.cpp @@ -138,6 +138,15 @@ std::string LabelFromValue(const UniValue& value) return label; } +void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, UniValue& entry) +{ + UniValue parent_descs(UniValue::VARR); + for (const auto& desc: wallet.GetWalletDescriptors(script_pubkey)) { + parent_descs.push_back(desc.descriptor->ToString()); + } + entry.pushKV("parent_descs", parent_descs); +} + void HandleWalletError(const std::shared_ptr wallet, DatabaseStatus& status, bilingual_str& error) { if (!wallet) { diff --git a/src/wallet/rpc/util.h b/src/wallet/rpc/util.h index c24cfe9f8485..d621308ee703 100644 --- a/src/wallet/rpc/util.h +++ b/src/wallet/rpc/util.h @@ -9,6 +9,8 @@ #include #include +#include