Line data Source code
1 : // Copyright (c) 2015-2020 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 <qt/test/wallettests.h>
6 : #include <qt/test/util.h>
7 :
8 : #include <interfaces/chain.h>
9 : #include <interfaces/node.h>
10 : #include <qt/bitcoinamountfield.h>
11 : #include <qt/clientmodel.h>
12 : #include <qt/optionsmodel.h>
13 : #include <qt/platformstyle.h>
14 : #include <qt/qvalidatedlineedit.h>
15 : #include <qt/sendcoinsdialog.h>
16 : #include <qt/sendcoinsentry.h>
17 : #include <qt/transactiontablemodel.h>
18 : #include <qt/transactionview.h>
19 : #include <qt/walletmodel.h>
20 : #include <key_io.h>
21 : #include <test/util/setup_common.h>
22 : #include <validation.h>
23 : #include <wallet/wallet.h>
24 : #include <qt/overviewpage.h>
25 : #include <qt/receivecoinsdialog.h>
26 : #include <qt/recentrequeststablemodel.h>
27 : #include <qt/receiverequestdialog.h>
28 :
29 : #include <memory>
30 :
31 : #include <QAbstractButton>
32 : #include <QAction>
33 : #include <QApplication>
34 : #include <QCheckBox>
35 : #include <QPushButton>
36 : #include <QTimer>
37 : #include <QVBoxLayout>
38 : #include <QTextEdit>
39 : #include <QListView>
40 : #include <QDialogButtonBox>
41 :
42 : namespace
43 : {
44 : //! Press "Yes" or "Cancel" buttons in modal send confirmation dialog.
45 0 : void ConfirmSend(QString* text = nullptr, bool cancel = false)
46 : {
47 0 : QTimer::singleShot(0, [text, cancel]() {
48 0 : for (QWidget* widget : QApplication::topLevelWidgets()) {
49 0 : if (widget->inherits("SendConfirmationDialog")) {
50 0 : SendConfirmationDialog* dialog = qobject_cast<SendConfirmationDialog*>(widget);
51 0 : if (text) *text = dialog->text();
52 0 : QAbstractButton* button = dialog->button(cancel ? QMessageBox::Cancel : QMessageBox::Yes);
53 0 : button->setEnabled(true);
54 0 : button->click();
55 0 : }
56 0 : }
57 0 : });
58 0 : }
59 :
60 : //! Send coins to address and return txid.
61 0 : uint256 SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf)
62 : {
63 0 : QVBoxLayout* entries = sendCoinsDialog.findChild<QVBoxLayout*>("entries");
64 0 : SendCoinsEntry* entry = qobject_cast<SendCoinsEntry*>(entries->itemAt(0)->widget());
65 0 : entry->findChild<QValidatedLineEdit*>("payTo")->setText(QString::fromStdString(EncodeDestination(address)));
66 0 : entry->findChild<BitcoinAmountField*>("payAmount")->setValue(amount);
67 0 : sendCoinsDialog.findChild<QFrame*>("frameFee")
68 0 : ->findChild<QFrame*>("frameFeeSelection")
69 0 : ->findChild<QCheckBox*>("optInRBF")
70 0 : ->setCheckState(rbf ? Qt::Checked : Qt::Unchecked);
71 0 : uint256 txid;
72 0 : boost::signals2::scoped_connection c(wallet.NotifyTransactionChanged.connect([&txid](CWallet*, const uint256& hash, ChangeType status) {
73 0 : if (status == CT_NEW) txid = hash;
74 0 : }));
75 0 : ConfirmSend();
76 0 : bool invoked = QMetaObject::invokeMethod(&sendCoinsDialog, "on_sendButton_clicked");
77 0 : assert(invoked);
78 : return txid;
79 0 : }
80 :
81 : //! Find index of txid in transaction list.
82 0 : QModelIndex FindTx(const QAbstractItemModel& model, const uint256& txid)
83 : {
84 0 : QString hash = QString::fromStdString(txid.ToString());
85 0 : int rows = model.rowCount({});
86 0 : for (int row = 0; row < rows; ++row) {
87 0 : QModelIndex index = model.index(row, 0, {});
88 0 : if (model.data(index, TransactionTableModel::TxHashRole) == hash) {
89 0 : return index;
90 : }
91 : }
92 0 : return {};
93 0 : }
94 :
95 : //! Invoke bumpfee on txid and check results.
96 0 : void BumpFee(TransactionView& view, const uint256& txid, bool expectDisabled, std::string expectError, bool cancel)
97 : {
98 0 : QTableView* table = view.findChild<QTableView*>("transactionView");
99 0 : QModelIndex index = FindTx(*table->selectionModel()->model(), txid);
100 0 : QVERIFY2(index.isValid(), "Could not find BumpFee txid");
101 :
102 : // Select row in table, invoke context menu, and make sure bumpfee action is
103 : // enabled or disabled as expected.
104 0 : QAction* action = view.findChild<QAction*>("bumpFeeAction");
105 0 : table->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
106 0 : action->setEnabled(expectDisabled);
107 0 : table->customContextMenuRequested({});
108 0 : QCOMPARE(action->isEnabled(), !expectDisabled);
109 :
110 0 : action->setEnabled(true);
111 0 : QString text;
112 0 : if (expectError.empty()) {
113 0 : ConfirmSend(&text, cancel);
114 : } else {
115 0 : ConfirmMessage(&text);
116 : }
117 0 : action->trigger();
118 0 : QVERIFY(text.indexOf(QString::fromStdString(expectError)) != -1);
119 0 : }
120 :
121 : //! Simple qt wallet tests.
122 : //
123 : // Test widgets can be debugged interactively calling show() on them and
124 : // manually running the event loop, e.g.:
125 : //
126 : // sendCoinsDialog.show();
127 : // QEventLoop().exec();
128 : //
129 : // This also requires overriding the default minimal Qt platform:
130 : //
131 : // QT_QPA_PLATFORM=xcb src/qt/test/test_bitcoin-qt # Linux
132 : // QT_QPA_PLATFORM=windows src/qt/test/test_bitcoin-qt # Windows
133 : // QT_QPA_PLATFORM=cocoa src/qt/test/test_bitcoin-qt # macOS
134 0 : void TestGUI(interfaces::Node& node)
135 : {
136 : // Set up wallet and chain with 105 blocks (5 mature blocks for spending).
137 0 : TestChain100Setup test;
138 0 : for (int i = 0; i < 5; ++i) {
139 0 : test.CreateAndProcessBlock({}, GetScriptForRawPubKey(test.coinbaseKey.GetPubKey()));
140 : }
141 0 : node.setContext(&test.m_node);
142 0 : std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(node.context()->chain.get(), "", CreateMockWalletDatabase());
143 0 : bool firstRun;
144 0 : wallet->LoadWallet(firstRun);
145 : {
146 0 : auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan();
147 0 : LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore);
148 0 : wallet->SetAddressBook(GetDestinationForKey(test.coinbaseKey.GetPubKey(), wallet->m_default_address_type), "", "receive");
149 0 : spk_man->AddKeyPubKey(test.coinbaseKey, test.coinbaseKey.GetPubKey());
150 0 : wallet->SetLastBlockProcessed(105, ::ChainActive().Tip()->GetBlockHash());
151 0 : }
152 : {
153 0 : WalletRescanReserver reserver(*wallet);
154 0 : reserver.reserve();
155 0 : CWallet::ScanResult result = wallet->ScanForWalletTransactions(Params().GetConsensus().hashGenesisBlock, 0 /* block height */, {} /* max height */, reserver, true /* fUpdate */);
156 0 : QCOMPARE(result.status, CWallet::ScanResult::SUCCESS);
157 0 : QCOMPARE(result.last_scanned_block, ::ChainActive().Tip()->GetBlockHash());
158 0 : QVERIFY(result.last_failed_block.IsNull());
159 0 : }
160 0 : wallet->SetBroadcastTransactions(true);
161 :
162 : // Create widgets for sending coins and listing transactions.
163 0 : std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other"));
164 0 : SendCoinsDialog sendCoinsDialog(platformStyle.get());
165 0 : TransactionView transactionView(platformStyle.get());
166 0 : OptionsModel optionsModel;
167 0 : ClientModel clientModel(node, &optionsModel);
168 0 : AddWallet(wallet);
169 0 : WalletModel walletModel(interfaces::MakeWallet(wallet), clientModel, platformStyle.get());
170 0 : RemoveWallet(wallet, nullopt);
171 0 : sendCoinsDialog.setModel(&walletModel);
172 0 : transactionView.setModel(&walletModel);
173 :
174 : {
175 : // Check balance in send dialog
176 0 : QLabel* balanceLabel = sendCoinsDialog.findChild<QLabel*>("labelBalance");
177 0 : QString balanceText = balanceLabel->text();
178 0 : int unit = walletModel.getOptionsModel()->getDisplayUnit();
179 0 : CAmount balance = walletModel.wallet().getBalance();
180 0 : QString balanceComparison = BitcoinUnits::formatWithUnit(unit, balance, false, BitcoinUnits::SeparatorStyle::ALWAYS);
181 0 : QCOMPARE(balanceText, balanceComparison);
182 0 : }
183 :
184 : // Send two transactions, and verify they are added to transaction list.
185 0 : TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel();
186 0 : QCOMPARE(transactionTableModel->rowCount({}), 105);
187 0 : uint256 txid1 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 5 * COIN, false /* rbf */);
188 0 : uint256 txid2 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 10 * COIN, true /* rbf */);
189 0 : QCOMPARE(transactionTableModel->rowCount({}), 107);
190 0 : QVERIFY(FindTx(*transactionTableModel, txid1).isValid());
191 0 : QVERIFY(FindTx(*transactionTableModel, txid2).isValid());
192 :
193 : // Call bumpfee. Test disabled, canceled, enabled, then failing cases.
194 0 : BumpFee(transactionView, txid1, true /* expect disabled */, "not BIP 125 replaceable" /* expected error */, false /* cancel */);
195 0 : BumpFee(transactionView, txid2, false /* expect disabled */, {} /* expected error */, true /* cancel */);
196 0 : BumpFee(transactionView, txid2, false /* expect disabled */, {} /* expected error */, false /* cancel */);
197 0 : BumpFee(transactionView, txid2, true /* expect disabled */, "already bumped" /* expected error */, false /* cancel */);
198 :
199 : // Check current balance on OverviewPage
200 0 : OverviewPage overviewPage(platformStyle.get());
201 0 : overviewPage.setWalletModel(&walletModel);
202 0 : QLabel* balanceLabel = overviewPage.findChild<QLabel*>("labelBalance");
203 0 : QString balanceText = balanceLabel->text().trimmed();
204 0 : int unit = walletModel.getOptionsModel()->getDisplayUnit();
205 0 : CAmount balance = walletModel.wallet().getBalance();
206 0 : QString balanceComparison = BitcoinUnits::formatWithUnit(unit, balance, false, BitcoinUnits::SeparatorStyle::ALWAYS);
207 0 : QCOMPARE(balanceText, balanceComparison);
208 :
209 : // Check Request Payment button
210 0 : ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get());
211 0 : receiveCoinsDialog.setModel(&walletModel);
212 0 : RecentRequestsTableModel* requestTableModel = walletModel.getRecentRequestsTableModel();
213 :
214 : // Label input
215 0 : QLineEdit* labelInput = receiveCoinsDialog.findChild<QLineEdit*>("reqLabel");
216 0 : labelInput->setText("TEST_LABEL_1");
217 :
218 : // Amount input
219 0 : BitcoinAmountField* amountInput = receiveCoinsDialog.findChild<BitcoinAmountField*>("reqAmount");
220 0 : amountInput->setValue(1);
221 :
222 : // Message input
223 0 : QLineEdit* messageInput = receiveCoinsDialog.findChild<QLineEdit*>("reqMessage");
224 0 : messageInput->setText("TEST_MESSAGE_1");
225 0 : int initialRowCount = requestTableModel->rowCount({});
226 0 : QPushButton* requestPaymentButton = receiveCoinsDialog.findChild<QPushButton*>("receiveButton");
227 0 : requestPaymentButton->click();
228 0 : for (QWidget* widget : QApplication::topLevelWidgets()) {
229 0 : if (widget->inherits("ReceiveRequestDialog")) {
230 0 : ReceiveRequestDialog* receiveRequestDialog = qobject_cast<ReceiveRequestDialog*>(widget);
231 0 : QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("payment_header")->text(), QString("Payment information"));
232 0 : QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("uri_tag")->text(), QString("URI:"));
233 0 : QString uri = receiveRequestDialog->QObject::findChild<QLabel*>("uri_content")->text();
234 0 : QCOMPARE(uri.count("bitcoin:"), 2);
235 0 : QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("address_tag")->text(), QString("Address:"));
236 :
237 0 : QCOMPARE(uri.count("amount=0.00000001"), 2);
238 0 : QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_tag")->text(), QString("Amount:"));
239 0 : QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_content")->text(), QString("0.00000001 ") + QString::fromStdString(CURRENCY_UNIT));
240 :
241 0 : QCOMPARE(uri.count("label=TEST_LABEL_1"), 2);
242 0 : QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_tag")->text(), QString("Label:"));
243 0 : QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_content")->text(), QString("TEST_LABEL_1"));
244 :
245 0 : QCOMPARE(uri.count("message=TEST_MESSAGE_1"), 2);
246 0 : QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_tag")->text(), QString("Message:"));
247 0 : QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_content")->text(), QString("TEST_MESSAGE_1"));
248 0 : }
249 0 : }
250 :
251 : // Clear button
252 0 : QPushButton* clearButton = receiveCoinsDialog.findChild<QPushButton*>("clearButton");
253 0 : clearButton->click();
254 0 : QCOMPARE(labelInput->text(), QString(""));
255 0 : QCOMPARE(amountInput->value(), CAmount(0));
256 0 : QCOMPARE(messageInput->text(), QString(""));
257 :
258 : // Check addition to history
259 0 : int currentRowCount = requestTableModel->rowCount({});
260 0 : QCOMPARE(currentRowCount, initialRowCount+1);
261 :
262 : // Check Remove button
263 0 : QTableView* table = receiveCoinsDialog.findChild<QTableView*>("recentRequestsView");
264 0 : table->selectRow(currentRowCount-1);
265 0 : QPushButton* removeRequestButton = receiveCoinsDialog.findChild<QPushButton*>("removeRequestButton");
266 0 : removeRequestButton->click();
267 0 : QCOMPARE(requestTableModel->rowCount({}), currentRowCount-1);
268 0 : }
269 :
270 : } // namespace
271 :
272 1 : void WalletTests::walletTests()
273 : {
274 : #ifdef Q_OS_MAC
275 1 : if (QApplication::platformName() == "minimal") {
276 : // Disable for mac on "minimal" platform to avoid crashes inside the Qt
277 : // framework when it tries to look up unimplemented cocoa functions,
278 : // and fails to handle returned nulls
279 : // (https://bugreports.qt.io/browse/QTBUG-49686).
280 1 : QWARN("Skipping WalletTests on mac build with 'minimal' platform set due to Qt bugs. To run AppTests, invoke "
281 : "with 'QT_QPA_PLATFORM=cocoa test_bitcoin-qt' on mac, or else use a linux or windows build.");
282 1 : return;
283 : }
284 : #endif
285 0 : TestGUI(m_node);
286 1 : }
|