From dc1cc1c35995dc09085b3d9270c445b7923fdb51 Mon Sep 17 00:00:00 2001 From: furszy Date: Tue, 6 Dec 2022 12:39:00 -0300 Subject: gui: bugfix, getAvailableBalance skips selected coins The previous behavior for getAvailableBalance when coin control has selected coins was to return the sum of them. Instead, we are currently returning the wallet's available total balance minus the selected coins total amount. This turns into a GUI-only issue for the "use available balance" button when the user manually select coins in the send screen. Reason: We missed to update the GetAvailableBalance function to include the coin control selected coins on #25685. Context: Since #25685 we skip the selected coins inside `AvailableCoins`, the reason is that there is no need to traverse the wallet's txes map just to get coins that can directly be fetched by their id. --- src/bench/wallet_create_tx.cpp | 4 ++-- src/qt/sendcoinsdialog.cpp | 7 ++++++- src/wallet/interfaces.cpp | 20 +++++++++++++++++++- src/wallet/spend.cpp | 6 ------ src/wallet/spend.h | 2 -- src/wallet/test/wallet_tests.cpp | 2 +- 6 files changed, 28 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/bench/wallet_create_tx.cpp b/src/bench/wallet_create_tx.cpp index 80d23d1e51..cb31421598 100644 --- a/src/bench/wallet_create_tx.cpp +++ b/src/bench/wallet_create_tx.cpp @@ -102,7 +102,7 @@ static void WalletCreateTx(benchmark::Bench& bench, const OutputType output_type } // Check available balance - auto bal = wallet::GetAvailableBalance(wallet); // Cache + auto bal = WITH_LOCK(wallet.cs_wallet, return wallet::AvailableCoins(wallet).GetTotalAmount()); // Cache assert(bal == 50 * COIN * (chain_size - COINBASE_MATURITY)); wallet::CCoinControl coin_control; @@ -161,7 +161,7 @@ static void AvailableCoins(benchmark::Bench& bench, const std::vectorfAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner(); + // Same behavior as send: if we have selected coins, only obtain their available balance. + // Copy to avoid modifying the member's data. + CCoinControl coin_control = *m_coin_control; + coin_control.m_allow_other_inputs = !coin_control.HasSelected(); + // Calculate available amount to send. - CAmount amount = model->getAvailableBalance(m_coin_control.get()); + CAmount amount = model->getAvailableBalance(&coin_control); for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry* e = qobject_cast(ui->entries->itemAt(i)->widget()); if (e && !e->isHidden() && e != entry) { diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 1a76e46c54..df1eb19a33 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -403,7 +404,24 @@ public: CAmount getBalance() override { return GetBalance(*m_wallet).m_mine_trusted; } CAmount getAvailableBalance(const CCoinControl& coin_control) override { - return GetAvailableBalance(*m_wallet, &coin_control); + LOCK(m_wallet->cs_wallet); + CAmount total_amount = 0; + // Fetch selected coins total amount + if (coin_control.HasSelected()) { + FastRandomContext rng{}; + CoinSelectionParams params(rng); + // Note: for now, swallow any error. + if (auto res = FetchSelectedInputs(*m_wallet, coin_control, params)) { + total_amount += res->total_amount; + } + } + + // And fetch the wallet available coins + if (coin_control.m_allow_other_inputs) { + total_amount += AvailableCoins(*m_wallet, &coin_control).GetTotalAmount(); + } + + return total_amount; } isminetype txinIsMine(const CTxIn& txin) override { diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp index 4548d5f813..57f3785a3a 100644 --- a/src/wallet/spend.cpp +++ b/src/wallet/spend.cpp @@ -356,12 +356,6 @@ CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* return AvailableCoins(wallet, coinControl, /*feerate=*/ std::nullopt, params); } -CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinControl) -{ - LOCK(wallet.cs_wallet); - return AvailableCoins(wallet, coinControl).GetTotalAmount(); -} - const CTxOut& FindNonChangeParentOutput(const CWallet& wallet, const COutPoint& outpoint) { AssertLockHeld(wallet.cs_wallet); diff --git a/src/wallet/spend.h b/src/wallet/spend.h index 78c2c5f22b..cc9ccf3011 100644 --- a/src/wallet/spend.h +++ b/src/wallet/spend.h @@ -94,8 +94,6 @@ CoinsResult AvailableCoins(const CWallet& wallet, */ CoinsResult AvailableCoinsListUnspent(const CWallet& wallet, const CCoinControl* coinControl = nullptr, CoinFilterParams params = {}) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); -CAmount GetAvailableBalance(const CWallet& wallet, const CCoinControl* coinControl = nullptr); - /** * Find non-change parent output. */ diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 2e95a14807..edcfaa24e5 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -581,7 +581,7 @@ BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup) BOOST_CHECK_EQUAL(list.begin()->second.size(), 1U); // Check initial balance from one mature coinbase transaction. - BOOST_CHECK_EQUAL(50 * COIN, GetAvailableBalance(*wallet)); + BOOST_CHECK_EQUAL(50 * COIN, WITH_LOCK(wallet->cs_wallet, return AvailableCoins(*wallet).GetTotalAmount())); // Add a transaction creating a change address, and confirm ListCoins still // returns the coin associated with the change address underneath the -- cgit v1.2.3 From 74eac3a82fc948467d5a15a5af420b36ce8eb04a Mon Sep 17 00:00:00 2001 From: furszy Date: Wed, 14 Dec 2022 10:52:33 -0300 Subject: test: add coverage for 'useAvailableBalance' functionality The following cases were covered: Case 1: No coin control selected coins. - 'useAvailableBalance' should fill the amount edit box with the total available balance. Case 2: With coin control selected coins. - 'useAvailableBalance' should fill the amount edit box with the sum of the selected coins values. --- src/qt/sendcoinsdialog.h | 3 +++ src/qt/test/wallettests.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) (limited to 'src') diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index 2fcdf5b32a..ac05cd98e5 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -49,6 +49,9 @@ public: void pasteEntry(const SendCoinsRecipient &rv); bool handlePaymentRequest(const SendCoinsRecipient &recipient); + // Only used for testing-purposes + wallet::CCoinControl* getCoinControl() { return m_coin_control.get(); } + public Q_SLOTS: void clear(); void reject() override; diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index be5bcbbd54..25d09c1740 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -136,6 +137,42 @@ void CompareBalance(WalletModel& walletModel, CAmount expected_balance, QLabel* QCOMPARE(balance_label_to_check->text().trimmed(), balanceComparison); } +// Verify the 'useAvailableBalance' functionality. With and without manually selected coins. +// Case 1: No coin control selected coins. +// 'useAvailableBalance' should fill the amount edit box with the total available balance +// Case 2: With coin control selected coins. +// 'useAvailableBalance' should fill the amount edit box with the sum of the selected coins values. +void VerifyUseAvailableBalance(SendCoinsDialog& sendCoinsDialog, const WalletModel& walletModel) +{ + // Verify first entry amount and "useAvailableBalance" button + QVBoxLayout* entries = sendCoinsDialog.findChild("entries"); + QVERIFY(entries->count() == 1); // only one entry + SendCoinsEntry* send_entry = qobject_cast(entries->itemAt(0)->widget()); + QVERIFY(send_entry->getValue().amount == 0); + // Now click "useAvailableBalance", check updated balance (the entire wallet balance should be set) + Q_EMIT send_entry->useAvailableBalance(send_entry); + QVERIFY(send_entry->getValue().amount == walletModel.getCachedBalance().balance); + + // Now manually select two coins and click on "useAvailableBalance". Then check updated balance + // (only the sum of the selected coins should be set). + int COINS_TO_SELECT = 2; + auto coins = walletModel.wallet().listCoins(); + CAmount sum_selected_coins = 0; + int selected = 0; + QVERIFY(coins.size() == 1); // context check, coins received only on one destination + for (const auto& [outpoint, tx_out] : coins.begin()->second) { + sendCoinsDialog.getCoinControl()->Select(outpoint); + sum_selected_coins += tx_out.txout.nValue; + if (++selected == COINS_TO_SELECT) break; + } + QVERIFY(selected == COINS_TO_SELECT); + + // Now that we have 2 coins selected, "useAvailableBalance" should update the balance label only with + // the sum of them. + Q_EMIT send_entry->useAvailableBalance(send_entry); + QVERIFY(send_entry->getValue().amount == sum_selected_coins); +} + //! Simple qt wallet tests. // // Test widgets can be debugged interactively calling show() on them and @@ -207,6 +244,9 @@ void TestGUI(interfaces::Node& node) // Check balance in send dialog CompareBalance(walletModel, walletModel.wallet().getBalance(), sendCoinsDialog.findChild("labelBalance")); + // Check 'UseAvailableBalance' functionality + VerifyUseAvailableBalance(sendCoinsDialog, walletModel); + // Send two transactions, and verify they are added to transaction list. TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel(); QCOMPARE(transactionTableModel->rowCount({}), 105); -- cgit v1.2.3 From cd98b717398f7b13ace91ea9efac9ce1e60b4d62 Mon Sep 17 00:00:00 2001 From: furszy Date: Wed, 14 Dec 2022 22:30:59 -0300 Subject: gui: 'getAvailableBalance', include watch only balance Only for wallets with private keys disabled. The returned amount need to include the watch-only available balance too. Solves #26687. --- src/qt/walletmodel.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 61172d1625..565b732bf0 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -613,5 +613,17 @@ uint256 WalletModel::getLastBlockProcessed() const CAmount WalletModel::getAvailableBalance(const CCoinControl* control) { - return control && control->HasSelected() ? wallet().getAvailableBalance(*control) : getCachedBalance().balance; + // No selected coins, return the cached balance + if (!control || !control->HasSelected()) { + const interfaces::WalletBalances& balances = getCachedBalance(); + CAmount available_balance = balances.balance; + // if wallet private keys are disabled, this is a watch-only wallet + // so, let's include the watch-only balance. + if (balances.have_watch_only && m_wallet->privateKeysDisabled()) { + available_balance += balances.watch_only_balance; + } + return available_balance; + } + // Fetch balance from the wallet, taking into account the selected coins + return wallet().getAvailableBalance(*control); } -- cgit v1.2.3 From 2f76ac0383904123676f1b4eeba0f772a4c5cb5d Mon Sep 17 00:00:00 2001 From: furszy Date: Fri, 16 Dec 2022 23:24:18 -0300 Subject: test,gui: decouple chain and wallet initialization from test case Prepare ground for legacy watch-only test. --- src/qt/test/wallettests.cpp | 87 ++++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 25d09c1740..0f6a166ecc 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -173,6 +173,39 @@ void VerifyUseAvailableBalance(SendCoinsDialog& sendCoinsDialog, const WalletMod QVERIFY(send_entry->getValue().amount == sum_selected_coins); } +void SyncUpWallet(const std::shared_ptr& wallet, interfaces::Node& node) +{ + WalletRescanReserver reserver(*wallet); + reserver.reserve(); + CWallet::ScanResult result = wallet->ScanForWalletTransactions(Params().GetConsensus().hashGenesisBlock, /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/false); + QCOMPARE(result.status, CWallet::ScanResult::SUCCESS); + QCOMPARE(result.last_scanned_block, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash())); + QVERIFY(result.last_failed_block.IsNull()); +} + +std::shared_ptr SetupDescriptorsWallet(interfaces::Node& node, TestChain100Setup& test) +{ + std::shared_ptr wallet = std::make_shared(node.context()->chain.get(), "", CreateMockWalletDatabase()); + wallet->LoadWallet(); + LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet->SetupDescriptorScriptPubKeyMans(); + + // Add the coinbase key + FlatSigningProvider provider; + std::string error; + std::unique_ptr desc = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false); + assert(desc); + WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); + if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false); + CTxDestination dest = GetDestinationForKey(test.coinbaseKey.GetPubKey(), wallet->m_default_address_type); + wallet->SetAddressBook(dest, "", "receive"); + wallet->SetLastBlockProcessed(105, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash())); + SyncUpWallet(wallet, node); + wallet->SetBroadcastTransactions(true); + return wallet; +} + //! Simple qt wallet tests. // // Test widgets can be debugged interactively calling show() on them and @@ -186,44 +219,8 @@ void VerifyUseAvailableBalance(SendCoinsDialog& sendCoinsDialog, const WalletMod // QT_QPA_PLATFORM=xcb src/qt/test/test_bitcoin-qt # Linux // QT_QPA_PLATFORM=windows src/qt/test/test_bitcoin-qt # Windows // QT_QPA_PLATFORM=cocoa src/qt/test/test_bitcoin-qt # macOS -void TestGUI(interfaces::Node& node) +void TestGUI(interfaces::Node& node, const std::shared_ptr& wallet) { - // Set up wallet and chain with 105 blocks (5 mature blocks for spending). - TestChain100Setup test; - for (int i = 0; i < 5; ++i) { - test.CreateAndProcessBlock({}, GetScriptForRawPubKey(test.coinbaseKey.GetPubKey())); - } - auto wallet_loader = interfaces::MakeWalletLoader(*test.m_node.chain, *Assert(test.m_node.args)); - test.m_node.wallet_loader = wallet_loader.get(); - node.setContext(&test.m_node); - const std::shared_ptr wallet = std::make_shared(node.context()->chain.get(), "", CreateMockWalletDatabase()); - wallet->LoadWallet(); - wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); - { - LOCK(wallet->cs_wallet); - wallet->SetupDescriptorScriptPubKeyMans(); - - // Add the coinbase key - FlatSigningProvider provider; - std::string error; - std::unique_ptr desc = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false); - assert(desc); - WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); - if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false); - CTxDestination dest = GetDestinationForKey(test.coinbaseKey.GetPubKey(), wallet->m_default_address_type); - wallet->SetAddressBook(dest, "", "receive"); - wallet->SetLastBlockProcessed(105, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash())); - } - { - WalletRescanReserver reserver(*wallet); - reserver.reserve(); - CWallet::ScanResult result = wallet->ScanForWalletTransactions(Params().GetConsensus().hashGenesisBlock, /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/false); - QCOMPARE(result.status, CWallet::ScanResult::SUCCESS); - QCOMPARE(result.last_scanned_block, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash())); - QVERIFY(result.last_failed_block.IsNull()); - } - wallet->SetBroadcastTransactions(true); - // Create widgets for sending coins and listing transactions. std::unique_ptr platformStyle(PlatformStyle::instantiate("other")); SendCoinsDialog sendCoinsDialog(platformStyle.get()); @@ -353,6 +350,22 @@ void TestGUI(interfaces::Node& node) QCOMPARE(walletModel.wallet().getAddressReceiveRequests().size(), size_t{0}); } +void TestGUI(interfaces::Node& node) +{ + // Set up wallet and chain with 105 blocks (5 mature blocks for spending). + TestChain100Setup test; + for (int i = 0; i < 5; ++i) { + test.CreateAndProcessBlock({}, GetScriptForRawPubKey(test.coinbaseKey.GetPubKey())); + } + auto wallet_loader = interfaces::MakeWalletLoader(*test.m_node.chain, *Assert(test.m_node.args)); + test.m_node.wallet_loader = wallet_loader.get(); + node.setContext(&test.m_node); + + // "Full" GUI tests, use descriptor wallet + const std::shared_ptr& desc_wallet = SetupDescriptorsWallet(node, test); + TestGUI(node, desc_wallet); +} + } // namespace void WalletTests::walletTests() -- cgit v1.2.3 From 306aab5bb471904faed325d9f3b38b7e891c7bbb Mon Sep 17 00:00:00 2001 From: furszy Date: Sat, 17 Dec 2022 09:39:24 -0300 Subject: test,gui: decouple widgets and model into a MiniGui struct So it can be reused across tests. --- src/qt/test/wallettests.cpp | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 0f6a166ecc..d3d67969a5 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -206,6 +206,32 @@ std::shared_ptr SetupDescriptorsWallet(interfaces::Node& node, TestChai return wallet; } +struct MiniGUI { +public: + SendCoinsDialog sendCoinsDialog; + TransactionView transactionView; + OptionsModel optionsModel; + std::unique_ptr clientModel; + std::unique_ptr walletModel; + + MiniGUI(interfaces::Node& node, const PlatformStyle* platformStyle) : sendCoinsDialog(platformStyle), transactionView(platformStyle), optionsModel(node) { + bilingual_str error; + QVERIFY(optionsModel.Init(error)); + clientModel = std::make_unique(node, &optionsModel); + } + + void initModelForWallet(interfaces::Node& node, const std::shared_ptr& wallet, const PlatformStyle* platformStyle) + { + WalletContext& context = *node.walletLoader().context(); + AddWallet(context, wallet); + walletModel = std::make_unique(interfaces::MakeWallet(context, wallet), *clientModel, platformStyle); + RemoveWallet(context, wallet, /* load_on_start= */ std::nullopt); + sendCoinsDialog.setModel(walletModel.get()); + transactionView.setModel(walletModel.get()); + } + +}; + //! Simple qt wallet tests. // // Test widgets can be debugged interactively calling show() on them and @@ -223,18 +249,11 @@ void TestGUI(interfaces::Node& node, const std::shared_ptr& wallet) { // Create widgets for sending coins and listing transactions. std::unique_ptr platformStyle(PlatformStyle::instantiate("other")); - SendCoinsDialog sendCoinsDialog(platformStyle.get()); - TransactionView transactionView(platformStyle.get()); - OptionsModel optionsModel(node); - bilingual_str error; - QVERIFY(optionsModel.Init(error)); - ClientModel clientModel(node, &optionsModel); - WalletContext& context = *node.walletLoader().context(); - AddWallet(context, wallet); - WalletModel walletModel(interfaces::MakeWallet(context, wallet), clientModel, platformStyle.get()); - RemoveWallet(context, wallet, /* load_on_start= */ std::nullopt); - sendCoinsDialog.setModel(&walletModel); - transactionView.setModel(&walletModel); + MiniGUI mini_gui(node, platformStyle.get()); + mini_gui.initModelForWallet(node, wallet, platformStyle.get()); + WalletModel& walletModel = *mini_gui.walletModel; + SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog; + TransactionView& transactionView = mini_gui.transactionView; // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel. walletModel.pollBalanceChanged(); -- cgit v1.2.3 From 68eed5df8656bed1be6526b014e58d3123102b03 Mon Sep 17 00:00:00 2001 From: furszy Date: Fri, 16 Dec 2022 23:25:39 -0300 Subject: test,gui: add coverage for PSBT creation on legacy watch-only wallets --- src/qt/sendcoinsdialog.cpp | 2 +- src/qt/test/wallettests.cpp | 88 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 9c1d3f48e0..48f7fb6ad1 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -403,7 +403,7 @@ void SendCoinsDialog::presentPSBT(PartiallySignedTransaction& psbtx) CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); ssTx << psbtx; GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str()); - QMessageBox msgBox; + QMessageBox msgBox(this); //: Caption of "PSBT has been copied" messagebox msgBox.setText(tr("Unsigned Transaction", "PSBT copied")); msgBox.setInformativeText(tr("The PSBT has been copied to the clipboard. You can also save it.")); diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index d3d67969a5..eb7bf33a32 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include #include #include @@ -47,6 +49,7 @@ using wallet::CWallet; using wallet::CreateMockWalletDatabase; using wallet::RemoveWallet; using wallet::WALLET_FLAG_DESCRIPTORS; +using wallet::WALLET_FLAG_DISABLE_PRIVATE_KEYS; using wallet::WalletContext; using wallet::WalletDescriptor; using wallet::WalletRescanReserver; @@ -54,14 +57,14 @@ using wallet::WalletRescanReserver; namespace { //! Press "Yes" or "Cancel" buttons in modal send confirmation dialog. -void ConfirmSend(QString* text = nullptr, bool cancel = false) +void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_type = QMessageBox::Yes) { - QTimer::singleShot(0, [text, cancel]() { + QTimer::singleShot(0, [text, confirm_type]() { for (QWidget* widget : QApplication::topLevelWidgets()) { if (widget->inherits("SendConfirmationDialog")) { SendConfirmationDialog* dialog = qobject_cast(widget); if (text) *text = dialog->text(); - QAbstractButton* button = dialog->button(cancel ? QMessageBox::Cancel : QMessageBox::Yes); + QAbstractButton* button = dialog->button(confirm_type); button->setEnabled(true); button->click(); } @@ -70,7 +73,8 @@ void ConfirmSend(QString* text = nullptr, bool cancel = false) } //! Send coins to address and return txid. -uint256 SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf) +uint256 SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf, + QMessageBox::StandardButton confirm_type = QMessageBox::Yes) { QVBoxLayout* entries = sendCoinsDialog.findChild("entries"); SendCoinsEntry* entry = qobject_cast(entries->itemAt(0)->widget()); @@ -84,7 +88,7 @@ uint256 SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDe boost::signals2::scoped_connection c(wallet.NotifyTransactionChanged.connect([&txid](const uint256& hash, ChangeType status) { if (status == CT_NEW) txid = hash; })); - ConfirmSend(); + ConfirmSend(/*text=*/nullptr, confirm_type); bool invoked = QMetaObject::invokeMethod(&sendCoinsDialog, "sendButtonClicked", Q_ARG(bool, false)); assert(invoked); return txid; @@ -122,7 +126,7 @@ void BumpFee(TransactionView& view, const uint256& txid, bool expectDisabled, st action->setEnabled(true); QString text; if (expectError.empty()) { - ConfirmSend(&text, cancel); + ConfirmSend(&text, cancel ? QMessageBox::Cancel : QMessageBox::Yes); } else { ConfirmMessage(&text, 0ms); } @@ -183,6 +187,24 @@ void SyncUpWallet(const std::shared_ptr& wallet, interfaces::Node& node QVERIFY(result.last_failed_block.IsNull()); } +std::shared_ptr SetupLegacyWatchOnlyWallet(interfaces::Node& node, TestChain100Setup& test) +{ + std::shared_ptr wallet = std::make_shared(node.context()->chain.get(), "", CreateMockWalletDatabase()); + wallet->LoadWallet(); + { + LOCK(wallet->cs_wallet); + wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); + wallet->SetupLegacyScriptPubKeyMan(); + // Add watched key + CPubKey pubKey = test.coinbaseKey.GetPubKey(); + bool import_keys = wallet->ImportPubKeys({pubKey.GetID()}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*internal=*/false, /*timestamp=*/1); + assert(import_keys); + wallet->SetLastBlockProcessed(105, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash())); + } + SyncUpWallet(wallet, node); + return wallet; +} + std::shared_ptr SetupDescriptorsWallet(interfaces::Node& node, TestChain100Setup& test) { std::shared_ptr wallet = std::make_shared(node.context()->chain.get(), "", CreateMockWalletDatabase()); @@ -369,6 +391,56 @@ void TestGUI(interfaces::Node& node, const std::shared_ptr& wallet) QCOMPARE(walletModel.wallet().getAddressReceiveRequests().size(), size_t{0}); } +void TestGUIWatchOnly(interfaces::Node& node, TestChain100Setup& test) +{ + const std::shared_ptr& wallet = SetupLegacyWatchOnlyWallet(node, test); + + // Create widgets and init models + std::unique_ptr platformStyle(PlatformStyle::instantiate("other")); + MiniGUI mini_gui(node, platformStyle.get()); + mini_gui.initModelForWallet(node, wallet, platformStyle.get()); + WalletModel& walletModel = *mini_gui.walletModel; + SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog; + + // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel. + walletModel.pollBalanceChanged(); + // Check balance in send dialog + CompareBalance(walletModel, walletModel.wallet().getBalances().watch_only_balance, + sendCoinsDialog.findChild("labelBalance")); + + // Set change address + sendCoinsDialog.getCoinControl()->destChange = GetDestinationForKey(test.coinbaseKey.GetPubKey(), OutputType::LEGACY); + + // Time to reject "save" PSBT dialog ('SendCoins' locks the main thread until the dialog receives the event). + QTimer timer; + timer.setInterval(500); + QObject::connect(&timer, &QTimer::timeout, [&](){ + for (QWidget* widget : QApplication::topLevelWidgets()) { + if (widget->inherits("QMessageBox")) { + QMessageBox* dialog = qobject_cast(widget); + QAbstractButton* button = dialog->button(QMessageBox::Discard); + button->setEnabled(true); + button->click(); + timer.stop(); + break; + } + } + }); + timer.start(500); + + // Send tx and verify PSBT copied to the clipboard. + SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 5 * COIN, /*rbf=*/false, QMessageBox::Save); + const std::string& psbt_string = QApplication::clipboard()->text().toStdString(); + QVERIFY(!psbt_string.empty()); + + // Decode psbt + std::optional> decoded_psbt = DecodeBase64(psbt_string); + QVERIFY(decoded_psbt); + PartiallySignedTransaction psbt; + std::string err; + QVERIFY(DecodeRawPSBT(psbt, MakeByteSpan(*decoded_psbt), err)); +} + void TestGUI(interfaces::Node& node) { // Set up wallet and chain with 105 blocks (5 mature blocks for spending). @@ -383,6 +455,10 @@ void TestGUI(interfaces::Node& node) // "Full" GUI tests, use descriptor wallet const std::shared_ptr& desc_wallet = SetupDescriptorsWallet(node, test); TestGUI(node, desc_wallet); + + // Legacy watch-only wallet test + // Verify PSBT creation. + TestGUIWatchOnly(node, test); } } // namespace -- cgit v1.2.3