Bitcoin Core  24.1.0
P2P Digital Currency
wallettests.cpp
Go to the documentation of this file.
1 // Copyright (c) 2015-2021 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(), "", gArgs, 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, false /* rbf */);
214  uint256 txid2 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 10 * COIN, true /* rbf */);
215  QCOMPARE(transactionTableModel->rowCount({}), 107);
216  QVERIFY(FindTx(*transactionTableModel, txid1).isValid());
217  QVERIFY(FindTx(*transactionTableModel, txid2).isValid());
218 
219  // Call bumpfee. Test disabled, canceled, enabled, then failing cases.
220  BumpFee(transactionView, txid1, true /* expect disabled */, "not BIP 125 replaceable" /* expected error */, false /* cancel */);
221  BumpFee(transactionView, txid2, false /* expect disabled */, {} /* expected error */, true /* cancel */);
222  BumpFee(transactionView, txid2, false /* expect disabled */, {} /* expected error */, false /* cancel */);
223  BumpFee(transactionView, txid2, true /* expect disabled */, "already bumped" /* expected error */, false /* cancel */);
224 
225  // Check current balance on OverviewPage
226  OverviewPage overviewPage(platformStyle.get());
227  overviewPage.setWalletModel(&walletModel);
228  walletModel.pollBalanceChanged(); // Manual balance polling update
229  CompareBalance(walletModel, walletModel.wallet().getBalance(), overviewPage.findChild<QLabel*>("labelBalance"));
230 
231  // Check Request Payment button
232  ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get());
233  receiveCoinsDialog.setModel(&walletModel);
234  RecentRequestsTableModel* requestTableModel = walletModel.getRecentRequestsTableModel();
235 
236  // Label input
237  QLineEdit* labelInput = receiveCoinsDialog.findChild<QLineEdit*>("reqLabel");
238  labelInput->setText("TEST_LABEL_1");
239 
240  // Amount input
241  BitcoinAmountField* amountInput = receiveCoinsDialog.findChild<BitcoinAmountField*>("reqAmount");
242  amountInput->setValue(1);
243 
244  // Message input
245  QLineEdit* messageInput = receiveCoinsDialog.findChild<QLineEdit*>("reqMessage");
246  messageInput->setText("TEST_MESSAGE_1");
247  int initialRowCount = requestTableModel->rowCount({});
248  QPushButton* requestPaymentButton = receiveCoinsDialog.findChild<QPushButton*>("receiveButton");
249  requestPaymentButton->click();
250  QString address;
251  for (QWidget* widget : QApplication::topLevelWidgets()) {
252  if (widget->inherits("ReceiveRequestDialog")) {
253  ReceiveRequestDialog* receiveRequestDialog = qobject_cast<ReceiveRequestDialog*>(widget);
254  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("payment_header")->text(), QString("Payment information"));
255  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("uri_tag")->text(), QString("URI:"));
256  QString uri = receiveRequestDialog->QObject::findChild<QLabel*>("uri_content")->text();
257  QCOMPARE(uri.count("bitcoin:"), 2);
258  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("address_tag")->text(), QString("Address:"));
259  QVERIFY(address.isEmpty());
260  address = receiveRequestDialog->QObject::findChild<QLabel*>("address_content")->text();
261  QVERIFY(!address.isEmpty());
262 
263  QCOMPARE(uri.count("amount=0.00000001"), 2);
264  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_tag")->text(), QString("Amount:"));
265  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_content")->text(), QString::fromStdString("0.00000001 " + CURRENCY_UNIT));
266 
267  QCOMPARE(uri.count("label=TEST_LABEL_1"), 2);
268  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_tag")->text(), QString("Label:"));
269  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_content")->text(), QString("TEST_LABEL_1"));
270 
271  QCOMPARE(uri.count("message=TEST_MESSAGE_1"), 2);
272  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_tag")->text(), QString("Message:"));
273  QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_content")->text(), QString("TEST_MESSAGE_1"));
274  }
275  }
276 
277  // Clear button
278  QPushButton* clearButton = receiveCoinsDialog.findChild<QPushButton*>("clearButton");
279  clearButton->click();
280  QCOMPARE(labelInput->text(), QString(""));
281  QCOMPARE(amountInput->value(), CAmount(0));
282  QCOMPARE(messageInput->text(), QString(""));
283 
284  // Check addition to history
285  int currentRowCount = requestTableModel->rowCount({});
286  QCOMPARE(currentRowCount, initialRowCount+1);
287 
288  // Check addition to wallet
289  std::vector<std::string> requests = walletModel.wallet().getAddressReceiveRequests();
290  QCOMPARE(requests.size(), size_t{1});
291  RecentRequestEntry entry;
292  CDataStream{MakeUCharSpan(requests[0]), SER_DISK, CLIENT_VERSION} >> entry;
293  QCOMPARE(entry.nVersion, int{1});
294  QCOMPARE(entry.id, int64_t{1});
295  QVERIFY(entry.date.isValid());
296  QCOMPARE(entry.recipient.address, address);
297  QCOMPARE(entry.recipient.label, QString{"TEST_LABEL_1"});
298  QCOMPARE(entry.recipient.amount, CAmount{1});
299  QCOMPARE(entry.recipient.message, QString{"TEST_MESSAGE_1"});
300  QCOMPARE(entry.recipient.sPaymentRequest, std::string{});
301  QCOMPARE(entry.recipient.authenticatedMerchant, QString{});
302 
303  // Check Remove button
304  QTableView* table = receiveCoinsDialog.findChild<QTableView*>("recentRequestsView");
305  table->selectRow(currentRowCount-1);
306  QPushButton* removeRequestButton = receiveCoinsDialog.findChild<QPushButton*>("removeRequestButton");
307  removeRequestButton->click();
308  QCOMPARE(requestTableModel->rowCount({}), currentRowCount-1);
309 
310  // Check removal from wallet
311  QCOMPARE(walletModel.wallet().getAddressReceiveRequests().size(), size_t{0});
312 }
313 
314 } // namespace
315 
317 {
318 #ifdef Q_OS_MACOS
319  if (QApplication::platformName() == "minimal") {
320  // Disable for mac on "minimal" platform to avoid crashes inside the Qt
321  // framework when it tries to look up unimplemented cocoa functions,
322  // and fails to handle returned nulls
323  // (https://bugreports.qt.io/browse/QTBUG-49686).
324  QWARN("Skipping WalletTests on mac build with 'minimal' platform set due to Qt bugs. To run AppTests, invoke "
325  "with 'QT_QPA_PLATFORM=cocoa test_bitcoin-qt' on mac, or else use a linux or windows build.");
326  return;
327  }
328 #endif
329  TestGUI(m_node);
330 }
Widget for entering bitcoin amounts.
std::unique_ptr< interfaces::Chain > chain
Definition: context.h:57
Model for list of recently generated payment requests / bitcoin: URIs.
Dialog for requesting payment of bitcoins.
ArgsManager gArgs
Definition: system.cpp:86
OptionsModel * getOptionsModel() const
void setWalletModel(WalletModel *walletModel)
Unit
Bitcoin units.
Definition: bitcoinunits.h:42
interfaces::Wallet & wallet() const
Definition: walletmodel.h:145
assert(!tx.IsCoinBase())
Bilingual messages:
Definition: translation.h:18
CPubKey GetPubKey() const
Compute the public key from a private key.
Definition: key.cpp:187
TransactionTableModel * getTransactionTableModel() const
CScript GetScriptForRawPubKey(const CPubKey &pubKey)
Generate a P2PK script for the given pubkey.
Definition: standard.cpp:339
BitcoinUnit getDisplayUnit() const
Definition: optionsmodel.h:93
std::unique_ptr< Wallet > MakeWallet(wallet::WalletContext &context, const std::shared_ptr< wallet::CWallet > &wallet)
Return implementation of Wallet interface.
Definition: interfaces.cpp:630
RAII object to check and reserve a wallet rescan.
Definition: wallet.h:951
Double ended buffer combining vector and stream-like interfaces.
Definition: streams.h:185
Line edit that can be marked as "invalid" to show input validation feedback.
A single entry in the dialog for sending bitcoins.
int rowCount(const QModelIndex &parent) const override
static QString formatWithUnit(Unit unit, const CAmount &amount, bool plussign=false, SeparatorStyle separators=SeparatorStyle::STANDARD)
Format as string (with unit)
void ConfirmMessage(QString *text, std::chrono::milliseconds msec)
Press "Ok" button in message box dialog.
Definition: util.cpp:14
int64_t CAmount
Amount in satoshis (Can be negative)
Definition: amount.h:12
void setModel(WalletModel *model)
std::unique_ptr< Descriptor > Parse(const std::string &descriptor, FlatSigningProvider &out, std::string &error, bool require_checksum)
Parse a descriptor string.
std::unique_ptr< WalletLoader > MakeWalletLoader(Chain &chain, ArgsManager &args)
Return implementation of ChainClient interface for a wallet loader.
Definition: dummywallet.cpp:62
RecentRequestsTableModel * getRecentRequestsTableModel() const
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...
std::unique_ptr< WalletDatabase > CreateMockWalletDatabase(DatabaseOptions &options)
Return object for accessing temporary in-memory database.
Definition: walletdb.cpp:1242
#define LOCK(cs)
Definition: sync.h:261
ArgsManager * args
Definition: context.h:56
void walletTests()
Widget showing the transaction list for a wallet, including a filter row.
Indicate that this wallet supports DescriptorScriptPubKeyMan.
Definition: walletutil.h:66
WalletContext context
Dialog for sending bitcoins.
const std::string CURRENCY_UNIT
Definition: feerate.h:17
A CWallet maintains a set of transactions and balances, and provides the ability to create new transa...
Definition: wallet.h:235
std::variant< CNoDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, WitnessUnknown > CTxDestination
A txout script template with a specific destination.
Definition: standard.h:149
std::string ToString() const
Definition: uint256.cpp:64
Testing fixture that pre-creates a 100-block REGTEST-mode block chain.
Definition: setup_common.h:126
UI model for the transaction table of a wallet.
Model for Bitcoin network client.
Definition: clientmodel.h:54
Definition: node.h:39
void setModel(WalletModel *model)
#define WITH_LOCK(cs, code)
Run code while locking a mutex.
Definition: sync.h:305
Descriptor with some wallet metadata.
Definition: walletutil.h:76
Definition: init.h:25
bool RemoveWallet(WalletContext &context, const std::shared_ptr< CWallet > &wallet, std::optional< bool > load_on_start, std::vector< bilingual_str > &warnings)
Definition: wallet.cpp:122
interfaces::Node & m_node
Definition: wallettests.h:19
int rowCount(const QModelIndex &parent) const override
256-bit opaque blob.
Definition: uint256.h:119
static const PlatformStyle * instantiate(const QString &platformId)
Get style associated with provided platform name, or 0 if not known.
Interface from Qt to configuration data structure for Bitcoin client.
Definition: optionsmodel.h:40
const CChainParams & Params()
Return the currently selected parameters.
Interface to Bitcoin wallet from Qt view code.
Definition: walletmodel.h:52
WalletContext struct containing references to state shared between CWallet instances, like the reference to the chain interface, and the list of opened wallets.
Definition: context.h:35
constexpr auto MakeUCharSpan(V &&v) -> decltype(UCharSpanCast(Span
Like the Span constructor, but for (const) unsigned char member types only.
Definition: span.h:285
CTxDestination GetDestinationForKey(const CPubKey &key, OutputType type)
Get a destination of the requested type (if possible) to the specified key.
Definition: outputtype.cpp:51
std::string EncodeDestination(const CTxDestination &dest)
Definition: key_io.cpp:276
virtual CAmount getBalance()=0
Get balance.
virtual std::vector< std::string > getAddressReceiveRequests()=0
Get receive requests.
ChangeType
General change type (added, updated, removed).
Definition: ui_change_type.h:9
std::string EncodeSecret(const CKey &key)
Definition: key_io.cpp:216
interfaces::WalletLoader * wallet_loader
Reference to chain client that should used to load or create wallets opened by the gui...
Definition: context.h:62
static const int CLIENT_VERSION
bitcoind-res.rc includes this file, but it cannot cope with real c++ code.
Definition: clientversion.h:33
node::NodeContext m_node
Definition: setup_common.h:84
Top-level interface for a bitcoin node (bitcoind process).
Definition: node.h:69
bool AddWallet(WalletContext &context, const std::shared_ptr< CWallet > &wallet)
Definition: wallet.cpp:110
bool error(const char *fmt, const Args &... args)
Definition: system.h:48
Overview ("home") page widget.
Definition: overviewpage.h:28
void setValue(const CAmount &value)
#define Assert(val)
Identity function.
Definition: check.h:74
void pollBalanceChanged()
Definition: walletmodel.cpp:97
static constexpr CAmount COIN
The amount of satoshis in one BTC.
Definition: amount.h:15