diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 2ccd514dbf0..33264805ea1 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -369,11 +369,14 @@ struct WalletBalances CAmount balance = 0; CAmount unconfirmed_balance = 0; CAmount immature_balance = 0; + CAmount used_balance = 0; + CAmount nonmempool_balance = 0; bool balanceChanged(const WalletBalances& prev) const { return balance != prev.balance || unconfirmed_balance != prev.unconfirmed_balance || - immature_balance != prev.immature_balance; + immature_balance != prev.immature_balance || + used_balance != prev.used_balance || nonmempool_balance != prev.nonmempool_balance; } }; diff --git a/src/qt/bitcoinunits.cpp b/src/qt/bitcoinunits.cpp index f73d83e4fc0..4fe32585bab 100644 --- a/src/qt/bitcoinunits.cpp +++ b/src/qt/bitcoinunits.cpp @@ -142,7 +142,6 @@ QString BitcoinUnits::formatHtmlWithUnit(Unit unit, const CAmount& amount, bool QString BitcoinUnits::formatWithPrivacy(Unit unit, const CAmount& amount, SeparatorStyle separators, bool privacy) { - assert(amount >= 0); QString value; if (privacy) { value = format(unit, 0, false, separators, true).replace('0', '#'); diff --git a/src/qt/forms/overviewpage.ui b/src/qt/forms/overviewpage.ui index f44c26ef32f..9459de017d3 100644 --- a/src/qt/forms/overviewpage.ui +++ b/src/qt/forms/overviewpage.ui @@ -130,14 +130,14 @@ - + Qt::Horizontal - + Total: @@ -183,7 +183,33 @@ - + + + + IBeamCursor + + + Balance for wallet transactions not in the mempool + + + 0.00000000 BTC + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Non-mempool: + + + + IBeamCursor diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 36b7c417407..c6378e7f982 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -190,13 +190,20 @@ void OverviewPage::setBalance(const interfaces::WalletBalances& balances) ui->labelBalance->setText(BitcoinUnits::formatWithPrivacy(unit, balances.balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy)); ui->labelUnconfirmed->setText(BitcoinUnits::formatWithPrivacy(unit, balances.unconfirmed_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy)); ui->labelImmature->setText(BitcoinUnits::formatWithPrivacy(unit, balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy)); - ui->labelTotal->setText(BitcoinUnits::formatWithPrivacy(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy)); + ui->labelNonMempool->setText(BitcoinUnits::formatWithPrivacy(unit, balances.nonmempool_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy)); + ui->labelTotal->setText(BitcoinUnits::formatWithPrivacy(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance + balances.nonmempool_balance, BitcoinUnits::SeparatorStyle::ALWAYS, m_privacy)); // only show immature (newly mined) balance if it's non-zero, so as not to complicate things // for the non-mining users bool showImmature = balances.immature_balance != 0; ui->labelImmature->setVisible(showImmature); ui->labelImmatureText->setVisible(showImmature); + + // likewise for non-mempool balances + bool showNonMempool = balances.nonmempool_balance != 0; + + ui->labelNonMempool->setVisible(showNonMempool); + ui->labelNonMempoolText->setVisible(showNonMempool); } void OverviewPage::setClientModel(ClientModel *model) @@ -296,5 +303,6 @@ void OverviewPage::setMonospacedFont(const QFont& f) ui->labelBalance->setFont(f); ui->labelUnconfirmed->setFont(f); ui->labelImmature->setFont(f); + ui->labelNonMempool->setFont(f); ui->labelTotal->setFont(f); } diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 5e65dcca318..5c140694b2c 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -302,7 +302,7 @@ void TestGUI(interfaces::Node& node, const std::shared_ptr& wallet) OverviewPage overviewPage(platformStyle.get()); overviewPage.setWalletModel(&walletModel); walletModel.pollBalanceChanged(); // Manual balance polling update - CompareBalance(walletModel, walletModel.wallet().getBalance(), overviewPage.findChild("labelBalance")); + CompareBalance(walletModel, walletModel.wallet().getBalances().balance, overviewPage.findChild("labelBalance")); // Check Request Payment button ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get()); diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 4dc873347ff..b3e2f57b562 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -383,11 +383,13 @@ class WalletImpl : public Wallet } WalletBalances getBalances() override { - const auto bal = GetBalance(*m_wallet); + const auto bal = GetBalance(*m_wallet, /*min_depth=*/0, /*avoid_reuse=*/true, /*include_nonmempool=*/true); WalletBalances result; result.balance = bal.m_mine_trusted; result.unconfirmed_balance = bal.m_mine_untrusted_pending; result.immature_balance = bal.m_mine_immature; + result.used_balance = bal.m_mine_used; + result.nonmempool_balance = bal.m_mine_nonmempool; return result; } bool tryGetBalances(WalletBalances& balances, uint256& block_hash) override diff --git a/src/wallet/receive.cpp b/src/wallet/receive.cpp index 13a25b36326..cccc2be045f 100644 --- a/src/wallet/receive.cpp +++ b/src/wallet/receive.cpp @@ -242,7 +242,7 @@ bool CachedTxIsTrusted(const CWallet& wallet, const CWalletTx& wtx) return CachedTxIsTrusted(wallet, wtx, trusted_parents); } -Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse) +Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse, bool include_nonmempool) { Balance ret; bool allow_used_addresses = !avoid_reuse || !wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE); @@ -255,17 +255,38 @@ Balance GetBalance(const CWallet& wallet, const int min_depth, bool avoid_reuse) const bool is_trusted{CachedTxIsTrusted(wallet, wtx, trusted_parents)}; const int tx_depth{wallet.GetTxDepthInMainChain(wtx)}; - if (!wallet.IsSpent(outpoint) && (allow_used_addresses || !wallet.IsSpentKey(txo.GetTxOut().scriptPubKey))) { - // Get the amounts for mine - CAmount credit_mine = txo.GetTxOut().nValue; + bool nonmempool_spent = false; + switch (wallet.HowSpent(outpoint)) { + case CWallet::SpendType::CONFIRMED: + case CWallet::SpendType::MEMPOOL: + // treat as spent; ignore + break; + case CWallet::SpendType::NONMEMPOOL: + if (!include_nonmempool || !allow_used_addresses) break; + nonmempool_spent = true; + [[fallthrough]]; + case CWallet::SpendType::UNSPENT: + CAmount* bucket = nullptr; // Set the amounts in the return object if (wallet.IsTxImmatureCoinBase(wtx) && wtx.isConfirmed()) { - ret.m_mine_immature += credit_mine; + bucket = &ret.m_mine_immature; } else if (is_trusted && tx_depth >= min_depth) { - ret.m_mine_trusted += credit_mine; + bucket = &ret.m_mine_trusted; } else if (!is_trusted && wtx.InMempool()) { - ret.m_mine_untrusted_pending += credit_mine; + bucket = &ret.m_mine_untrusted_pending; + } + if (bucket) { + // Get the amounts for mine + CAmount credit_mine = txo.GetTxOut().nValue; + + if (!allow_used_addresses && wallet.IsSpentKey(txo.GetTxOut().scriptPubKey)) { + bucket = &ret.m_mine_used; + } + *bucket += credit_mine; + if (nonmempool_spent) { + ret.m_mine_nonmempool -= credit_mine; + } } } } diff --git a/src/wallet/receive.h b/src/wallet/receive.h index f6c55a45360..14f0a9bdf27 100644 --- a/src/wallet/receive.h +++ b/src/wallet/receive.h @@ -47,8 +47,10 @@ struct Balance { CAmount m_mine_trusted{0}; //!< Trusted, at depth=GetBalance.min_depth or more CAmount m_mine_untrusted_pending{0}; //!< Untrusted, but in mempool (pending) CAmount m_mine_immature{0}; //!< Immature coinbases in the main chain + CAmount m_mine_used{0}; //!< Trusted/untrusted/immature funds in utxos that have already been spent from (only populated if AVOID REUSE wallet flag is set) + CAmount m_mine_nonmempool{0}; //!< Coins spent by wallet txs that are not in the mempool }; -Balance GetBalance(const CWallet& wallet, int min_depth = 0, bool avoid_reuse = true); +Balance GetBalance(const CWallet& wallet, int min_depth = 0, bool avoid_reuse = true, bool include_nonmempool = false); std::map GetAddressBalances(const CWallet& wallet); std::set> GetAddressGroupings(const CWallet& wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); diff --git a/src/wallet/rpc/coins.cpp b/src/wallet/rpc/coins.cpp index 4aef0dda886..0ac60ad4f96 100644 --- a/src/wallet/rpc/coins.cpp +++ b/src/wallet/rpc/coins.cpp @@ -413,6 +413,7 @@ RPCHelpMan getbalances() {RPCResult::Type::STR_AMOUNT, "trusted", "trusted balance (outputs created by the wallet or confirmed outputs)"}, {RPCResult::Type::STR_AMOUNT, "untrusted_pending", "untrusted pending balance (outputs created by others that are in the mempool)"}, {RPCResult::Type::STR_AMOUNT, "immature", "balance from immature coinbase outputs"}, + {RPCResult::Type::STR_AMOUNT, "nonmempool", "sum of coins that are locked or spent by transactions not in the mempool (usually an over-estimate due to not accounting for change or spends that conflict with each other)"}, {RPCResult::Type::STR_AMOUNT, "used", /*optional=*/true, "(only present if avoid_reuse is set) balance from coins sent to addresses that were previously spent from (potentially privacy violating)"}, }}, RESULT_LAST_PROCESSED_BLOCK, @@ -433,18 +434,17 @@ RPCHelpMan getbalances() LOCK(wallet.cs_wallet); - const auto bal = GetBalance(wallet); + const auto bal = GetBalance(wallet, /*min_depth=*/0, /*avoid_reuse=*/true, /*include_nonmempool=*/true); + UniValue balances{UniValue::VOBJ}; { UniValue balances_mine{UniValue::VOBJ}; balances_mine.pushKV("trusted", ValueFromAmount(bal.m_mine_trusted)); balances_mine.pushKV("untrusted_pending", ValueFromAmount(bal.m_mine_untrusted_pending)); balances_mine.pushKV("immature", ValueFromAmount(bal.m_mine_immature)); + balances_mine.pushKV("nonmempool", ValueFromAmount(bal.m_mine_nonmempool)); if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) { - // If the AVOID_REUSE flag is set, bal has been set to just the un-reused address balance. Get - // the total balance, and then subtract bal to get the reused address balance. - const auto full_bal = GetBalance(wallet, 0, false); - balances_mine.pushKV("used", ValueFromAmount(full_bal.m_mine_trusted + full_bal.m_mine_untrusted_pending - bal.m_mine_trusted - bal.m_mine_untrusted_pending)); + balances_mine.pushKV("used", ValueFromAmount(bal.m_mine_used)); } balances.pushKV("mine", std::move(balances_mine)); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index d08d6782c1b..37e9ef13ca2 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -738,6 +738,29 @@ bool CWallet::IsSpent(const COutPoint& outpoint) const return false; } +CWallet::SpendType CWallet::HowSpent(const COutPoint& outpoint) const +{ + SpendType st{SpendType::UNSPENT}; + + std::pair range; + range = mapTxSpends.equal_range(outpoint); + + for (TxSpends::const_iterator it = range.first; it != range.second; ++it) { + const Txid& txid = it->second; + const auto mit = mapWallet.find(txid); + if (mit != mapWallet.end()) { + const auto& wtx = mit->second; + if (wtx.isConfirmed()) return SpendType::CONFIRMED; + if (wtx.InMempool()) { + st = SpendType::MEMPOOL; + } else if (!wtx.isAbandoned() && !wtx.isBlockConflicted() && !wtx.isMempoolConflicted()) { + if (st == SpendType::UNSPENT) st = SpendType::NONMEMPOOL; + } + } + } + return st; +} + void CWallet::AddToSpends(const COutPoint& outpoint, const Txid& txid) { mapTxSpends.insert(std::make_pair(outpoint, txid)); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index b341ac6da2d..e114f01f2e4 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -556,6 +556,13 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati int GetTxBlocksToMaturity(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsTxImmatureCoinBase(const CWalletTx& wtx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + enum class SpendType { + UNSPENT, + CONFIRMED, + MEMPOOL, + NONMEMPOOL, + }; + SpendType HowSpent(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsSpent(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); // Whether this or any known scriptPubKey with the same single key has been spent. diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py index f5650b87f01..2aa79c95bf7 100755 --- a/test/functional/wallet_abandonconflict.py +++ b/test/functional/wallet_abandonconflict.py @@ -115,10 +115,9 @@ def run_test(self): # inputs are still spent, but change not received newbalance = alice.getbalance() assert_equal(newbalance, balance - signed3_change) - # Unconfirmed received funds that are not in mempool, also shouldn't show - # up in unconfirmed balance + # Unconfirmed received funds that are not in mempool balances = alice.getbalances()['mine'] - assert_equal(balances['untrusted_pending'] + balances['trusted'], newbalance) + assert_equal(balances['untrusted_pending'] + balances['trusted'] + balances['nonmempool'], newbalance) # Also shouldn't show up in listunspent assert not txABC2 in [utxo["txid"] for utxo in alice.listunspent(0)] balance = newbalance diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index 8c83f42ecfd..62f900e1348 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -145,10 +145,12 @@ def test_balances(*, fee_node_1=0): # getbalances expected_balances_0 = {'mine': {'immature': Decimal('0E-8'), 'trusted': Decimal('9.99'), # change from node 0's send - 'untrusted_pending': Decimal('60.0')}} + 'untrusted_pending': Decimal('60.0'), + 'nonmempool': Decimal('0.0')}} expected_balances_1 = {'mine': {'immature': Decimal('0E-8'), 'trusted': Decimal('0E-8'), # node 1's send had an unsafe input - 'untrusted_pending': Decimal('30.0') - fee_node_1}} # Doesn't include output of node 0's send since it was spent + 'untrusted_pending': Decimal('30.0') - fee_node_1, # Doesn't include output of node 0's send since it was spent + 'nonmempool': Decimal('0.0')}} balances_0 = self.nodes[0].getbalances() balances_1 = self.nodes[1].getbalances() # remove lastprocessedblock keys (they will be tested later) diff --git a/test/functional/wallet_conflicts.py b/test/functional/wallet_conflicts.py index b16a2f83d2a..a6562be9360 100755 --- a/test/functional/wallet_conflicts.py +++ b/test/functional/wallet_conflicts.py @@ -304,8 +304,9 @@ def test_mempool_and_block_conflicts(self): bob.sendrawtransaction(tx1_conflict_conflict) # kick tx1_conflict out of the mempool bob.sendrawtransaction(raw_tx1) #re-broadcast tx1 because it is no longer conflicted - # Now bob has no pending funds because tx1 and tx2 are spent by tx3, which hasn't been re-broadcast yet - assert_equal(bob.getbalances()["mine"]["untrusted_pending"], 0) + # Now bob has pending funds because tx1 and tx2 are spent by tx3, which hasn't been re-broadcast yet + bob_bal = bob.getbalances()["mine"] + assert_equal(bob_bal["untrusted_pending"], -bob_bal["nonmempool"]) bob.sendrawtransaction(raw_tx3) assert_equal(len(bob.getrawmempool()), 4) # The mempool contains: tx1, tx2, tx1_conflict_conflict, tx3 diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py index 89989fbf81b..a55f4943a0a 100755 --- a/test/functional/wallet_migration.py +++ b/test/functional/wallet_migration.py @@ -8,6 +8,7 @@ import shutil import struct import time +from decimal import Decimal from test_framework.address import ( key_to_p2pkh, @@ -522,6 +523,7 @@ def test_pk_coinbases(self): self.generatetodescriptor(self.master_node, 1, desc) bals = wallet.getbalances() + bals["mine"]["nonmempool"] = Decimal('0.0') _, wallet = self.migrate_and_get_rpc("pkcb") @@ -537,6 +539,7 @@ def test_encrypted(self): txid = default.sendtoaddress(addr, 1) self.generate(self.master_node, 1) bals = wallet.getbalances() + bals["mine"]["nonmempool"] = Decimal('0.0') # Use self.migrate_and_get_rpc to test this error to get everything copied over to the master node assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", self.migrate_and_get_rpc, "encrypted") @@ -577,6 +580,7 @@ def test_unloaded_by_path(self): txid = default.sendtoaddress(addr, 1) self.generate(self.master_node, 1) bals = wallet.getbalances() + bals["mine"]["nonmempool"] = Decimal('0.0') wallet.unloadwallet() @@ -616,6 +620,7 @@ def test_wallet_with_relative_path(self): txid = default.sendtoaddress(addr, 1) self.generate(self.master_node, 1) bals = wallet.getbalances() + bals["mine"]["nonmempool"] = Decimal('0.0') migrate_res, wallet = self.migrate_and_get_rpc(relative_name) @@ -636,6 +641,7 @@ def test_wallet_with_relative_path(self): self.old_node.restorewallet("relative_restored", migrate_res['backup_path']) wallet = self.old_node.get_wallet_rpc("relative_restored") assert wallet.gettransaction(txid) + del bals["mine"]["nonmempool"] assert_equal(bals, wallet.getbalances()) info = wallet.getwalletinfo() @@ -652,6 +658,7 @@ def test_wallet_with_path(self, wallet_path): txid = default.sendtoaddress(addr, 1) self.generate(self.master_node, 1) bals = wallet.getbalances() + bals["mine"]["nonmempool"] = Decimal('0.0') _, wallet = self.migrate_and_get_rpc(wallet_path) @@ -1423,14 +1430,14 @@ def test_miniscript(self): _, wallet = self.migrate_and_get_rpc("miniscript") # The miniscript with all keys should be in the migrated wallet - assert_equal(wallet.getbalances()["mine"], {"trusted": 0.75, "untrusted_pending": 0, "immature": 0}) + assert_equal(wallet.getbalances()["mine"], {"trusted": 0.75, "untrusted_pending": 0, "immature": 0, "nonmempool": 0}) assert_equal(wallet.getaddressinfo(all_keys_addr)["ismine"], True) assert_equal(wallet.getaddressinfo(some_keys_addr)["ismine"], False) # The miniscript with some keys should be in the watchonly wallet assert "miniscript_watchonly" in self.master_node.listwallets() watchonly = self.master_node.get_wallet_rpc("miniscript_watchonly") - assert_equal(watchonly.getbalances()["mine"], {"trusted": 1, "untrusted_pending": 0, "immature": 0}) + assert_equal(watchonly.getbalances()["mine"], {"trusted": 1, "untrusted_pending": 0, "immature": 0, "nonmempool": 0}) assert_equal(watchonly.getaddressinfo(some_keys_addr)["ismine"], True) assert_equal(watchonly.getaddressinfo(all_keys_addr)["ismine"], False) @@ -1479,7 +1486,7 @@ def test_taproot(self): res, wallet = self.migrate_and_get_rpc("taproot") # The rawtr should be migrated - assert_equal(wallet.getbalances()["mine"], {"trusted": 0.5, "untrusted_pending": 0, "immature": 0}) + assert_equal(wallet.getbalances()["mine"], {"trusted": 0.5, "untrusted_pending": 0, "immature": 0, "nonmempool": 0}) assert_equal(wallet.getaddressinfo(rawtr_addr)["ismine"], True) assert_equal(wallet.getaddressinfo(tr_addr)["ismine"], False) assert_equal(wallet.getaddressinfo(tr_script_addr)["ismine"], False) @@ -1487,7 +1494,7 @@ def test_taproot(self): # The tr() with some keys should be in the watchonly wallet assert "taproot_watchonly" in self.master_node.listwallets() watchonly = self.master_node.get_wallet_rpc("taproot_watchonly") - assert_equal(watchonly.getbalances()["mine"], {"trusted": 5, "untrusted_pending": 0, "immature": 0}) + assert_equal(watchonly.getbalances()["mine"], {"trusted": 5, "untrusted_pending": 0, "immature": 0, "nonmempool": 0}) assert_equal(watchonly.getaddressinfo(rawtr_addr)["ismine"], False) assert_equal(watchonly.getaddressinfo(tr_addr)["ismine"], True) assert_equal(watchonly.getaddressinfo(tr_script_addr)["ismine"], True) diff --git a/test/functional/wallet_orphanedreward.py b/test/functional/wallet_orphanedreward.py index f13b5a8c1b8..bd02010fa7c 100755 --- a/test/functional/wallet_orphanedreward.py +++ b/test/functional/wallet_orphanedreward.py @@ -46,6 +46,7 @@ def run_test(self): "trusted": 10, "untrusted_pending": 0, "immature": 0, + "nonmempool": 0, }) # And the unconfirmed tx to be abandoned assert_equal(self.nodes[1].gettransaction(txid)["details"][0]["abandoned"], True) diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 1316aecbf7a..b5119dc8bb5 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -32,8 +32,8 @@ def set_test_params(self): self.noban_tx_relay = True self.supports_cli = False self.extra_args = [ - ["-walletrbf=1"], - ["-walletrbf=1"] + ["-walletrbf=1", "-datacarriersize=16"], + ["-walletrbf=1", "-datacarriersize=16"] ] getcontext().prec = 8 # Satoshi precision for Decimal @@ -45,7 +45,7 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, conf_target=None, estimate_mode=None, fee_rate=None, add_to_wallet=None, psbt=None, inputs=None, add_inputs=None, include_unsafe=None, change_address=None, change_position=None, change_type=None, locktime=None, lock_unspents=None, replaceable=None, subtract_fee_from_outputs=None, - expect_error=None, solving_data=None, minconf=None): + expect_error=None, solving_data=None, minconf=None, nonmempool=False): assert_not_equal((amount is None), (data is None)) from_balance_before = from_wallet.getbalances()["mine"]["trusted"] @@ -171,16 +171,20 @@ def test_send(self, from_wallet, to_wallet=None, amount=None, data=None, tx = from_wallet.gettransaction(res["txid"]) assert tx assert_equal(tx["bip125-replaceable"], "yes" if replaceable else "no") - # Ensure transaction exists in the mempool: - tx = from_wallet.getrawtransaction(res["txid"], True) - assert tx - if amount: - if subtract_fee_from_outputs: - assert_equal(from_balance_before - from_balance, amount) - else: - assert_greater_than(from_balance_before - from_balance, amount) + if nonmempool: + assert_raises_rpc_error(-5, "No such mempool transaction", from_wallet.getrawtransaction, res["txid"]) + assert from_wallet.getbalances()["mine"]["nonmempool"] < 0 else: - assert next((out for out in tx["vout"] if out["scriptPubKey"]["asm"] == "OP_RETURN 35"), None) + # Ensure transaction exists in the mempool: + tx = from_wallet.getrawtransaction(res["txid"], True) + assert tx + if amount: + if subtract_fee_from_outputs: + assert_equal(from_balance_before - from_balance, amount) + else: + assert_greater_than(from_balance_before - from_balance, amount) + else: + assert next((out for out in tx["vout"] if out["scriptPubKey"]["asm"] == "OP_RETURN 35"), None) else: assert_equal(from_balance_before, from_balance) @@ -280,6 +284,9 @@ def run_test(self): res = w2.walletprocesspsbt(res["psbt"]) assert res["complete"] + self.log.info("Create mempool-invalid tx (due to large OP_RETURN)...") + self.test_send(from_wallet=w0, data=b"The quick brown fox jumps over the lazy dog".hex(), nonmempool=True) + self.log.info("Test setting explicit fee rate") res1 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, arg_fee_rate="1", add_to_wallet=False) res2 = self.test_send(from_wallet=w0, to_wallet=w1, amount=1, fee_rate="1", add_to_wallet=False) diff --git a/test/functional/wallet_v3_txs.py b/test/functional/wallet_v3_txs.py index db9f1483aba..79d8ad66738 100755 --- a/test/functional/wallet_v3_txs.py +++ b/test/functional/wallet_v3_txs.py @@ -39,19 +39,19 @@ def wrapper(self, *args): func(self, *args) finally: self.generate(self.nodes[0], 1) - try: - self.alice.sendall([self.charlie.getnewaddress()]) - except JSONRPCException as e: - assert "Total value of UTXO pool too low to pay for transaction" in e.error['message'] - try: - self.bob.sendall([self.charlie.getnewaddress()]) - except JSONRPCException as e: - assert "Total value of UTXO pool too low to pay for transaction" in e.error['message'] + for wallet in [self.alice, self.bob]: + txs = set(tx["txid"] for tx in wallet.listtransactions("*", 1000) if tx["confirmations"] == 0 and not tx["abandoned"]) + for tx in txs: + wallet.abandontransaction(tx) + try: + wallet.sendall([self.charlie.getnewaddress()]) + except JSONRPCException as e: + assert "Total value of UTXO pool too low to pay for transaction" in e.error['message'] self.generate(self.nodes[0], 1) for wallet in [self.alice, self.bob]: balance = wallet.getbalances()["mine"] - for balance_type in ["untrusted_pending", "trusted", "immature"]: + for balance_type in ["untrusted_pending", "trusted", "immature", "nonmempool"]: assert_equal(balance[balance_type], 0) assert_equal(self.alice.getrawmempool(), [])