diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp index bed9ec029aa..4ba6285d937 100644 --- a/src/wallet/rpc/addresses.cpp +++ b/src/wallet/rpc/addresses.cpp @@ -545,6 +545,10 @@ RPCHelpMan getaddressinfo() { {RPCResult::Type::STR, "label name", "Label name (defaults to \"\")."}, }}, + {RPCResult::Type::ARR, "use_txids", "", + { + {RPCResult::Type::STR_HEX, "txid", "The ids of transactions involving this wallet which received with the address"}, + }}, } }, RPCExamples{ @@ -638,6 +642,15 @@ RPCHelpMan getaddressinfo() } ret.pushKV("labels", std::move(labels)); + // NOTE: Intentionally not special-casing a single txid: while addresses + // should never be reused, it's not unexpected to have RBF result in + // multiple txids for a single use. + UniValue use_txids(UniValue::VARR); + pwallet->FindScriptPubKeyUsed(std::set{scriptPubKey}, [&use_txids](const CWalletTx&wtx) { + use_txids.push_back(wtx.GetHash().GetHex()); + }); + ret.pushKV("use_txids", std::move(use_txids)); + return ret; }, }; diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 8f4171eb15d..d3b572454b9 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -786,6 +786,60 @@ void CWallet::AddToSpends(const CWalletTx& wtx, WalletBatch* batch) AddToSpends(txin.prevout, wtx.GetHash(), batch); } +void CWallet::InitialiseAddressBookUsed() +{ + for (const auto& entry : mapWallet) { + const CWalletTx& wtx = entry.second; + UpdateAddressBookUsed(wtx); + } +} + +void CWallet::UpdateAddressBookUsed(const CWalletTx& wtx) +{ + for (const auto& output : wtx.tx->vout) { + CTxDestination dest; + if (!ExtractDestination(output.scriptPubKey, dest)) continue; + m_address_book[dest].m_used = true; + } +} + +bool CWallet::FindScriptPubKeyUsed(const std::set& keys, const std::variant, std::function>& callback) const +{ + AssertLockHeld(cs_wallet); + bool found_any = false; + for (const auto& key : keys) { + CTxDestination dest; + if (!ExtractDestination(key, dest)) continue; + const auto& address_book_it = m_address_book.find(dest); + if (address_book_it == m_address_book.end()) continue; + if (address_book_it->second.m_used) { + found_any = true; + break; + } + } + if (!found_any) return false; + if (std::holds_alternative(callback)) return true; + + found_any = false; + for (const auto& entry : mapWallet) { + const CWalletTx& wtx = entry.second; + for (size_t i = 0; i < wtx.tx->vout.size(); ++i) { + const auto& output = wtx.tx->vout[i]; + if (keys.count(output.scriptPubKey)) { + found_any = true; + const auto callback_type = callback.index(); + if (callback_type == 1) { + std::get>(callback)(wtx); + break; + } + std::get>(callback)(wtx, i); + } + } + } + + return found_any; +} + bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) { if (IsCrypted()) @@ -1083,6 +1137,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const // Update birth time when tx time is older than it. MaybeUpdateBirthTime(wtx.GetTxTime()); + UpdateAddressBookUsed(wtx); } if (!fInsertedNew) @@ -2338,7 +2393,7 @@ void CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::ve } } -DBErrors CWallet::LoadWallet() +DBErrors CWallet::LoadWallet(const do_init_used_flag do_init_used_flag_val) { LOCK(cs_wallet); @@ -2360,7 +2415,13 @@ DBErrors CWallet::LoadWallet() assert(m_internal_spk_managers.empty()); } - return nLoadWalletRet; + if (nLoadWalletRet != DBErrors::LOAD_OK) { + return nLoadWalletRet; + } + + if (do_init_used_flag_val == do_init_used_flag::Init) InitialiseAddressBookUsed(); + + return DBErrors::LOAD_OK; } util::Result CWallet::RemoveTxs(std::vector& txs_to_remove) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 6a998fa3983..2e4e9e47539 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -47,6 +47,7 @@ #include #include #include +#include #include #include @@ -237,6 +238,12 @@ struct CAddressBookData */ std::optional label; + /** Whether address is the destination of any wallet transation. + * Unlike other fields in address data struct, the used value is determined + * at runtime and not serialized as part of address data. + */ + bool m_used{false}; + /** * Address purpose which was originally recorded for payment protocol * support but now serves as a cached IsMine value. Wallet code should @@ -337,6 +344,9 @@ private: void AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void AddToSpends(const CWalletTx& wtx, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void InitialiseAddressBookUsed() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void UpdateAddressBookUsed(const CWalletTx&) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** * Add a transaction to the wallet, or update it. confirm.block_* should * be set when the transaction was known to be included in a block. When @@ -546,6 +556,8 @@ public: bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void ListLockedCoins(std::vector& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + bool FindScriptPubKeyUsed(const std::set& keys, const std::variant, std::function>& callback = std::monostate()) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /* * Rescan abort properties */ @@ -788,7 +800,8 @@ public: CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const; void chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) override; - DBErrors LoadWallet(); + enum class do_init_used_flag { Init, Skip }; + DBErrors LoadWallet(const do_init_used_flag do_init_used_flag_val = do_init_used_flag::Init); /** Erases the provided transactions from the wallet. */ util::Result RemoveTxs(std::vector& txs_to_remove) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index cda344ab19c..962a54dacc5 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -47,7 +47,7 @@ static void WalletCreate(CWallet* wallet_instance, uint64_t wallet_creation_flag wallet_instance->TopUpKeyPool(); } -static std::shared_ptr MakeWallet(const std::string& name, const fs::path& path, DatabaseOptions options) +static std::shared_ptr MakeWallet(const std::string& name, const fs::path& path, DatabaseOptions options, CWallet::do_init_used_flag do_init_used_flag_val = CWallet::do_init_used_flag::Init) { DatabaseStatus status; bilingual_str error; @@ -61,7 +61,7 @@ static std::shared_ptr MakeWallet(const std::string& name, const fs::pa std::shared_ptr wallet_instance{new CWallet(/*chain=*/nullptr, name, std::move(database)), WalletToolReleaseWallet}; DBErrors load_wallet_ret; try { - load_wallet_ret = wallet_instance->LoadWallet(); + load_wallet_ret = wallet_instance->LoadWallet(do_init_used_flag_val); } catch (const std::runtime_error&) { tfm::format(std::cerr, "Error loading %s. Is wallet being used by another process?\n", name); return nullptr; @@ -167,7 +167,8 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) DatabaseOptions options; ReadDatabaseArgs(args, options); options.require_existing = true; - const std::shared_ptr wallet_instance = MakeWallet(name, path, options); + // NOTE: We need to skip initialisation of the m_used flag, or else the address book count might be wrong + const std::shared_ptr wallet_instance = MakeWallet(name, path, options, CWallet::do_init_used_flag::Skip); if (!wallet_instance) return false; WalletShowInfo(wallet_instance.get()); wallet_instance->Close(); diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 56228d2bada..2b5db62f4eb 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -655,6 +655,16 @@ class WalletTest(BitcoinTestFramework): assert not address_info["iswatchonly"] assert not address_info["isscript"] assert not address_info["ischange"] + assert_equal(address_info['use_txids'], []) + + # Test getaddressinfo 'use_txids' field + addr = "mneYUmWYsuk7kySiURxCi3AGxrAqZxLgPZ" + txid_1 = self.nodes[0].sendtoaddress(addr, 1) + address_info = self.nodes[0].getaddressinfo(addr) + assert_equal(address_info['use_txids'], [txid_1]) + txid_2 = self.nodes[0].sendtoaddress(addr, 1) + address_info = self.nodes[0].getaddressinfo(addr) + assert_equal(sorted(address_info['use_txids']), sorted([txid_1, txid_2])) # Test getaddressinfo 'ischange' field on change address. self.generate(self.nodes[0], 1, sync_fun=self.no_op)