Bitcoin Core  24.99.0
P2P Digital Currency
wallettests.cpp
Go to the documentation of this file.
1 // Copyright (c) 2015-2022 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 <key_io.h>
11 #include <qt/bitcoinamountfield.h>
12 #include <qt/bitcoinunits.h>
13 #include <qt/clientmodel.h>
14 #include <qt/optionsmodel.h>
15 #include <qt/overviewpage.h>
16 #include <qt/platformstyle.h>
17 #include <qt/qvalidatedlineedit.h>
18 #include <qt/receivecoinsdialog.h>
21 #include <qt/sendcoinsdialog.h>
22 #include <qt/sendcoinsentry.h>
24 #include <qt/transactionview.h>
25 #include <qt/walletmodel.h>
26 #include <test/util/setup_common.h>
27 #include <validation.h>
28 #include <wallet/wallet.h>
29 
30 #include <chrono>
31 #include <memory>
32 
33 #include <QAbstractButton>
34 #include <QAction>
35 #include <QApplication>
36 #include <QCheckBox>
37 #include <QPushButton>
38 #include <QTimer>
39 #include <QVBoxLayout>
40 #include <QTextEdit>
41 #include <QListView>
42 #include <QDialogButtonBox>
43 
44 using wallet::AddWallet;
45 using wallet::CWallet;
52 
53 namespace
54 {
56 void ConfirmSend(QString* text = nullptr, bool cancel = false)
57 {
58  QTimer::singleShot(0, [text, cancel]() {
59  for (QWidget* widget : QApplication::topLevelWidgets()) {
60  if (widget->inherits("SendConfirmationDialog")) {
61  SendConfirmationDialog* dialog = qobject_cast<SendConfirmationDialog*>(widget);
62  if (text) *text = dialog->text();
63  QAbstractButton* button = dialog->button(cancel ? QMessageBox::Cancel : QMessageBox::Yes);
64  button->setEnabled(true);
65  button->click();
66  }
67  }
68  });
69 }
70 
72 uint256 SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf)
73 {
74  QVBoxLayout* entries = sendCoinsDialog.findChild<QVBoxLayout*>("entries");
75  SendCoinsEntry* entry = qobject_cast<SendCoinsEntry*>(entries->itemAt(0)->widget());
76  entry->findChild<QValidatedLineEdit*>("payTo")->setText(QString::fromStdString(EncodeDestination(address)));
77  entry->findChild<BitcoinAmountField*>("payAmount")->setValue(amount);
78  sendCoinsDialog.findChild<QFrame*>("frameFee")
79  ->findChild<QFrame*>("frameFeeSelection")
80  ->findChild<QCheckBox*>("optInRBF")
81  ->setCheckState(rbf ? Qt::Checked : Qt::Unchecked);
82  uint256 txid;
83  boost::signals2::scoped_connection c(wallet.NotifyTransactionChanged.connect([&txid](const uint256& hash, ChangeType status) {
84  if (status == CT_NEW) txid = hash;
85  }));
86  ConfirmSend();
87  bool invoked = QMetaObject::invokeMethod(&sendCoinsDialog, "sendButtonClicked", Q_ARG(bool, false));
88  assert(invoked);
89  return txid;
90 }
91 
93 QModelIndex FindTx(const QAbstractItemModel& model, const uint256& txid)
94 {
95  QString hash = QString::fromStdString(txid.ToString());
96  int rows = model.rowCount({});
97  for (int row = 0; row < rows; ++row) {
98  QModelIndex index = model.index(row, 0, {});
99  if (model.data(index, TransactionTableModel::TxHashRole) == hash) {
100  return index;
101  }
102  }
103  return {};
104 }
105 
107 void BumpFee(TransactionView& view, const uint256& txid, bool expectDisabled, std::string expectError, bool cancel)
108 {
109  QTableView* table = view.findChild<QTableView*>("transactionView");
110  QModelIndex index = FindTx(*table->selectionModel()->model(), txid);
111  QVERIFY2(index.isValid(), "Could not find BumpFee txid");
112 
113  // Select row in table, invoke context menu, and make sure bumpfee action is
114  // enabled or disabled as expected.
115  QAction* action = view.findChild<QAction*>("bumpFeeAction");
116  table->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
117  action->setEnabled(expectDisabled);
118  table->customContextMenuRequested({});
119  QCOMPARE(action->isEnabled(), !expectDisabled);
120 
121  action->setEnabled(true);
122  QString text;
123  if (expectError.empty()) {
124  ConfirmSend(&text, cancel);
125  } else {
126  ConfirmMessage(&text, 0ms);
127  }
128  action->trigger();
129  QVERIFY(text.indexOf(QString::fromStdString(expectError)) != -1);
130 }
131 
132 void CompareBalance(WalletModel& walletModel, CAmount expected_balance, QLabel* balance_label_to_check)
133 {
134  BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit();
135  QString balanceComparison = BitcoinUnits::formatWithUnit(unit, expected_balance, false, BitcoinUnits::SeparatorStyle::ALWAYS);
136  QCOMPARE(balance_label_to_check->text().trimmed(), balanceComparison);
137 }
138 
140 //
141 // Test widgets can be debugged interactively calling show() on them and
142 // manually running the event loop, e.g.:
143 //
144 // sendCoinsDialog.show();
145 // QEventLoop().exec();
146 //
147 // This also requires overriding the default minimal Qt platform:
148 //
149 // QT_QPA_PLATFORM=xcb src/qt/test/test_bitcoin-qt # Linux
150 // QT_QPA_PLATFORM=windows src/qt/test/test_bitcoin-qt # Windows
151 // QT_QPA_PLATFORM=cocoa src/qt/test/test_bitcoin-qt # macOS
152 void TestGUI(interfaces::Node& node)
153 {
154  // Set up wallet and chain with 105 blocks (5 mature blocks for spending).
155  TestChain100Setup test;
156  for (int i = 0; i < 5; ++i) {
158  }
159  auto wallet_loader = interfaces::MakeWalletLoader(*test.m_node.chain, *Assert(test.m_node.args));
160  test.m_node.wallet_loader = wallet_loader.get();
161  node.setContext(&test.m_node);
162  const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(node.context()->chain.get(), "", CreateMockWalletDatabase());
163  wallet->LoadWallet();
164  wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
165  {
166  LOCK(wallet->cs_wallet);
167  wallet->SetupDescriptorScriptPubKeyMans();
168 
169  // Add the coinbase key
170  FlatSigningProvider provider;
171  std::string error;
172  std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false);
173  assert(desc);
174  WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1);
175  if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false);
176  CTxDestination dest = GetDestinationForKey(test.coinbaseKey.GetPubKey(), wallet->m_default_address_type);
177  wallet->SetAddressBook(dest, "", "receive");
178  wallet->SetLastBlockProcessed(105, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash()));
179  }
180  {
181  WalletRescanReserver reserver(*wallet);
182  reserver.reserve();
183  CWallet::ScanResult result = wallet->ScanForWalletTransactions(Params().GetConsensus().hashGenesisBlock, /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/false);
184  QCOMPARE(result.status, CWallet::ScanResult::SUCCESS);
185  QCOMPARE(result.last_scanned_block, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash()));
186  QVERIFY(result.last_failed_block.IsNull());
187  }
188  wallet->SetBroadcastTransactions(true);
189 
190  // Create widgets for sending coins and listing transactions.
191  std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other"));
192  SendCoinsDialog sendCoinsDialog(platformStyle.get());
193  TransactionView transactionView(platformStyle.get());
194  OptionsModel optionsModel(node);
196  QVERIFY(optionsModel.Init(error));
197  ClientModel clientModel(node, &optionsModel);
198  WalletContext& context = *node.walletLoader().context();
200  WalletModel walletModel(interfaces::MakeWallet(context, wallet), clientModel, platformStyle.get());
201  RemoveWallet(context, wallet, /* load_on_start= */ std::nullopt);
202  sendCoinsDialog.setModel(&walletModel);
203  transactionView.setModel(&walletModel);
204 
205  // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel.
206  walletModel.pollBalanceChanged();
207  // Check balance in send dialog
208  CompareBalance(walletModel, walletModel.wallet().getBalance(), sendCoinsDialog.findChild<QLabel*>("labelBalance"));
209 
210  // Send two transactions, and verify they are added to transaction list.
211  TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel();
212  QCOMPARE(transactionTableModel->rowCount({}), 105);
213  uint256 txid1 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 5 * COIN, /*rbf=*/false);
214  uint256 txid2 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 10 * COIN, /*rbf=*/true);
215  // Transaction table model updates on a QueuedConnection, so process events to ensure it's updated.
216  qApp->processEvents();
217  QCOMPARE(transactionTableModel->rowCount({}), 107);
218  QVERIFY(FindTx(*transactionTableModel, txid1).isValid());
219  QVERIFY(FindTx(*transactionTableModel, txid2).isValid());
220 
221  // Call bumpfee. Test disabled, canceled, enabled, then failing cases.
222  BumpFee(transactionView, txid1, /*expectDisabled=*/true, /*expectError=*/"not BIP 125 replaceable", /*cancel=*/false);
223  BumpFee(transactionView, txid2, /*expectDisabled=*/false, /*expectError=*/{}, /*cancel=*/true);
224  BumpFee(transactionView, txid2, /*expectDisabled=*/false, /*expectError=*/{}, /*cancel=*/false);
225  BumpFee(transactionView, txid2, /*expectDisabled=*/true, /*expectError=*/"already bumped", /*cancel=*/false);
226 
227  // Check current balance on OverviewPage
228  OverviewPage overviewPage(platformStyle.get());
229  overviewPage.setWalletModel(&walletModel);
230  walletModel.pollBalanceChanged(); // Manual balance polling update
231  CompareBalance(walletModel, walletModel.wallet().getBalance(), overviewPage.findChild<QLabel*>("labelBalance"));
232 
233  // Check Request Payment button
234  ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get());
235  receiveCoinsDialog.setModel(&walletModel);
236  RecentRequestsTableModel* requestTableModel = walletModel.getRecentRequestsTableModel();
237 
238  // Label input
239  QLineEdit* labelInput = receiveCoinsDialog.findChild<QLineEdit*>("reqLabel");
240  labelInput->setText("TEST_LABEL_1");
241 
242  // Amount input
243  BitcoinAmountField* amountInput = receiveCoinsDialog.findChild<BitcoinAmountField*>("reqAmount");
244  amountInput->setValue(1);
245 
246  // Message input
247  QLineEdit* messageInput = receiveCoinsDialog.findChild<QLineEdit*>("reqMessage");
248  messageInput->setText("TEST_MESSAGE_1");
249  int initialRowCount = requestTableModel->rowCount({});
250  QPushButton* requestPaymentButton = receiveCoinsDialog.findChild<QPushButton*>("receiveButton");
251  requestPaymentButton->click();
252  QString address;
253  for (QWidget* widget : QApplication::topLevelWidgets()) {
254  if (widget->inherits("ReceiveRequestDialog")) {
255  ReceiveRequestDialog* receiveRequestDialog = qobject_cast<ReceiveRequestDialog*>(widget);
256  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("payment_header")->text(), QString("Payment information"));
257  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("uri_tag")->text(), QString("URI:"));
258  QString uri = receiveRequestDialog->QObject::findChild<QLabel*>("uri_content")->text();
259  QCOMPARE(uri.count("bitcoin:"), 2);
260  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("address_tag")->text(), QString("Address:"));
261  QVERIFY(address.isEmpty());
262  address = receiveRequestDialog->QObject::findChild<QLabel*>("address_content")->text();
263  QVERIFY(!address.isEmpty());
264 
265  QCOMPARE(uri.count("amount=0.00000001"), 2);
266  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_tag")->text(), QString("Amount:"));
267  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_content")->text(), QString::fromStdString("0.00000001 " + CURRENCY_UNIT));
268 
269  QCOMPARE(uri.count("label=TEST_LABEL_1"), 2);
270  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_tag")->text(), QString("Label:"));
271  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_content")->text(), QString("TEST_LABEL_1"));
272 
273  QCOMPARE(uri.count("message=TEST_MESSAGE_1"), 2);
274  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_tag")->text(), QString("Message:"));
275  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_content")->text(), QString("TEST_MESSAGE_1"));
276  }
277  }
278 
279  // Clear button
280  QPushButton* clearButton = receiveCoinsDialog.findChild<QPushButton*>("clearButton");
281  clearButton->click();
282  QCOMPARE(labelInput->text(), QString(""));
283  QCOMPARE(amountInput->value(), CAmount(0));
284  QCOMPARE(messageInput->text(), QString(""));
285 
286  // Check addition to history
287  int currentRowCount = requestTableModel->rowCount({});
288  QCOMPARE(currentRowCount, initialRowCount+1);
289 
290  // Check addition to wallet
291  std::vector<std::string> requests = walletModel.wallet().getAddressReceiveRequests();
292  QCOMPARE(requests.size(), size_t{1});
293  RecentRequestEntry entry;
294  DataStream{MakeUCharSpan(requests[0])} >> entry;
295  QCOMPARE(entry.nVersion, int{1});
296  QCOMPARE(entry.id, int64_t{1});
297  QVERIFY(entry.date.isValid());
298  QCOMPARE(entry.recipient.address, address);
299  QCOMPARE(entry.recipient.label, QString{"TEST_LABEL_1"});
300  QCOMPARE(entry.recipient.amount, CAmount{1});
301  QCOMPARE(entry.recipient.message, QString{"TEST_MESSAGE_1"});
302  QCOMPARE(entry.recipient.sPaymentRequest, std::string{});
303  QCOMPARE(entry.recipient.authenticatedMerchant, QString{});
304 
305  // Check Remove button
306  QTableView* table = receiveCoinsDialog.findChild<QTableView*>("recentRequestsView");
307  table->selectRow(currentRowCount-1);
308  QPushButton* removeRequestButton = receiveCoinsDialog.findChild<QPushButton*>("removeRequestButton");
309  removeRequestButton->click();
310  QCOMPARE(requestTableModel->rowCount({}), currentRowCount-1);
311 
312  // Check removal from wallet
313  QCOMPARE(walletModel.wallet().getAddressReceiveRequests().size(), size_t{0});
314 }
315 
316 } // namespace
317 
319 {
320 #ifdef Q_OS_MACOS
321  if (QApplication::platformName() == "minimal") {
322  // Disable for mac on "minimal" platform to avoid crashes inside the Qt
323  // framework when it tries to look up unimplemented cocoa functions,
324  // and fails to handle returned nulls
325  // (https://bugreports.qt.io/browse/QTBUG-49686).
326  QWARN("Skipping WalletTests on mac build with 'minimal' platform set due to Qt bugs. To run AppTests, invoke "
327  "with 'QT_QPA_PLATFORM=cocoa test_bitcoin-qt' on mac, or else use a linux or windows build.");
328  return;
329  }
330 #endif
331  TestGUI(m_node);
332 }
int64_t CAmount
Amount in satoshis (Can be negative)
Definition: amount.h:12
static constexpr CAmount COIN
The amount of satoshis in one BTC.
Definition: amount.h:15
const CChainParams & Params()
Return the currently selected parameters.
Definition: chainparams.cpp:94
#define Assert(val)
Identity function.
Definition: check.h:73
Widget for entering bitcoin amounts.
void setValue(const CAmount &value)
static QString formatWithUnit(Unit unit, const CAmount &amount, bool plussign=false, SeparatorStyle separators=SeparatorStyle::STANDARD)
Format as string (with unit)
Unit
Bitcoin units.
Definition: bitcoinunits.h:42
CPubKey GetPubKey() const
Compute the public key from a private key.
Definition: key.cpp:187
Model for Bitcoin network client.
Definition: clientmodel.h:54
Double ended buffer combining vector and stream-like interfaces.
Definition: streams.h:186
Interface from Qt to configuration data structure for Bitcoin client.
Definition: optionsmodel.h:41
BitcoinUnit getDisplayUnit() const
Definition: optionsmodel.h:94
Overview ("home") page widget.
Definition: overviewpage.h:29
static const PlatformStyle * instantiate(const QString &platformId)
Get style associated with provided platform name, or 0 if not known.
Line edit that can be marked as "invalid" to show input validation feedback.
Dialog for requesting payment of bitcoins.
int64_t id
SendCoinsRecipient recipient
int nVersion
QDateTime date
Model for list of recently generated payment requests / bitcoin: URIs.
int rowCount(const QModelIndex &parent) const override
Dialog for sending bitcoins.
void setModel(WalletModel *model)
A single entry in the dialog for sending bitcoins.
std::string sPaymentRequest
UI model for the transaction table of a wallet.
@ TxHashRole
Transaction hash.
int rowCount(const QModelIndex &parent) const override
Widget showing the transaction list for a wallet, including a filter row.
Interface to Bitcoin wallet from Qt view code.
Definition: walletmodel.h:53
RecentRequestsTableModel * getRecentRequestsTableModel() const
void pollBalanceChanged()
Definition: walletmodel.cpp:93
interfaces::Wallet & wallet() const
Definition: walletmodel.h:143
TransactionTableModel * getTransactionTableModel() const
OptionsModel * getOptionsModel() const
void walletTests()
interfaces::Node & m_node
Definition: wallettests.h:19
std::string ToString() const
Definition: uint256.cpp:55
Top-level interface for a bitcoin node (bitcoind process).
Definition: node.h:70
virtual std::vector< std::string > getAddressReceiveRequests()=0
Get receive requests.
virtual CAmount getBalance()=0
Get balance.
256-bit opaque blob.
Definition: uint256.h:105
A CWallet maintains a set of transactions and balances, and provides the ability to create new transa...
Definition: wallet.h:237
Descriptor with some wallet metadata.
Definition: walletutil.h:77
RAII object to check and reserve a wallet rescan.
Definition: wallet.h:957
std::unique_ptr< Descriptor > Parse(const std::string &descriptor, FlatSigningProvider &out, std::string &error, bool require_checksum)
Parse a descriptor string.
const std::string CURRENCY_UNIT
Definition: feerate.h:17
std::string EncodeSecret(const CKey &key)
Definition: key_io.cpp:216
std::string EncodeDestination(const CTxDestination &dest)
Definition: key_io.cpp:276
bool error(const char *fmt, const Args &... args)
Definition: logging.h:261
std::unique_ptr< WalletLoader > MakeWalletLoader(Chain &chain, ArgsManager &args)
Return implementation of ChainClient interface for a wallet loader.
Definition: dummywallet.cpp:62
std::unique_ptr< Wallet > MakeWallet(wallet::WalletContext &context, const std::shared_ptr< wallet::CWallet > &wallet)
Return implementation of Wallet interface.
Definition: interfaces.cpp:631
Definition: init.h:25
Definition: node.h:39
std::unique_ptr< WalletDatabase > CreateMockWalletDatabase(DatabaseOptions &options)
Return object for accessing temporary in-memory database.
Definition: walletdb.cpp:1242
bool AddWallet(WalletContext &context, const std::shared_ptr< CWallet > &wallet)
Definition: wallet.cpp:112
@ WALLET_FLAG_DESCRIPTORS
Indicate that this wallet supports DescriptorScriptPubKeyMan.
Definition: walletutil.h:66
bool RemoveWallet(WalletContext &context, const std::shared_ptr< CWallet > &wallet, std::optional< bool > load_on_start, std::vector< bilingual_str > &warnings)
Definition: wallet.cpp:124
WalletContext context
CTxDestination GetDestinationForKey(const CPubKey &key, OutputType type)
Get a destination of the requested type (if possible) to the specified key.
Definition: outputtype.cpp:53
void ConfirmMessage(QString *text, std::chrono::milliseconds msec)
Press "Ok" button in message box dialog.
Definition: util.cpp:14
constexpr auto MakeUCharSpan(V &&v) -> decltype(UCharSpanCast(Span{std::forward< V >(v)}))
Like the Span constructor, but for (const) unsigned char member types only.
Definition: span.h:285
CScript GetScriptForRawPubKey(const CPubKey &pubKey)
Generate a P2PK script for the given pubkey.
Definition: standard.cpp:339
std::variant< CNoDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, WitnessUnknown > CTxDestination
A txout script template with a specific destination.
Definition: standard.h:149
node::NodeContext m_node
Definition: setup_common.h:80
Testing fixture that pre-creates a 100-block REGTEST-mode block chain.
Definition: setup_common.h:128
CBlock CreateAndProcessBlock(const std::vector< CMutableTransaction > &txns, const CScript &scriptPubKey, Chainstate *chainstate=nullptr)
Create a new block with just given transactions, coinbase paying to scriptPubKey, and try to add it t...
Bilingual messages:
Definition: translation.h:18
ArgsManager * args
Definition: context.h:56
interfaces::WalletLoader * wallet_loader
Reference to chain client that should used to load or create wallets opened by the gui.
Definition: context.h:62
std::unique_ptr< interfaces::Chain > chain
Definition: context.h:57
WalletContext struct containing references to state shared between CWallet instances,...
Definition: context.h:35
#define LOCK(cs)
Definition: sync.h:258
#define WITH_LOCK(cs, code)
Run code while locking a mutex.
Definition: sync.h:302
ChangeType
General change type (added, updated, removed).
Definition: ui_change_type.h:9
assert(!tx.IsCoinBase())