Bitcoin Core 22.99.0
P2P Digital Currency
wallettests.cpp
Go to the documentation of this file.
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
6#include <qt/test/util.h>
7
8#include <interfaces/chain.h>
9#include <interfaces/node.h>
11#include <qt/clientmodel.h>
12#include <qt/optionsmodel.h>
13#include <qt/platformstyle.h>
15#include <qt/sendcoinsdialog.h>
16#include <qt/sendcoinsentry.h>
18#include <qt/transactionview.h>
19#include <qt/walletmodel.h>
20#include <key_io.h>
22#include <validation.h>
23#include <wallet/wallet.h>
24#include <qt/overviewpage.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
42namespace
43{
45void ConfirmSend(QString* text = nullptr, bool cancel = false)
46{
47 QTimer::singleShot(0, [text, cancel]() {
48 for (QWidget* widget : QApplication::topLevelWidgets()) {
49 if (widget->inherits("SendConfirmationDialog")) {
50 SendConfirmationDialog* dialog = qobject_cast<SendConfirmationDialog*>(widget);
51 if (text) *text = dialog->text();
52 QAbstractButton* button = dialog->button(cancel ? QMessageBox::Cancel : QMessageBox::Yes);
53 button->setEnabled(true);
54 button->click();
55 }
56 }
57 });
58}
59
61uint256 SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf)
62{
63 QVBoxLayout* entries = sendCoinsDialog.findChild<QVBoxLayout*>("entries");
64 SendCoinsEntry* entry = qobject_cast<SendCoinsEntry*>(entries->itemAt(0)->widget());
65 entry->findChild<QValidatedLineEdit*>("payTo")->setText(QString::fromStdString(EncodeDestination(address)));
66 entry->findChild<BitcoinAmountField*>("payAmount")->setValue(amount);
67 sendCoinsDialog.findChild<QFrame*>("frameFee")
68 ->findChild<QFrame*>("frameFeeSelection")
69 ->findChild<QCheckBox*>("optInRBF")
70 ->setCheckState(rbf ? Qt::Checked : Qt::Unchecked);
71 uint256 txid;
72 boost::signals2::scoped_connection c(wallet.NotifyTransactionChanged.connect([&txid](const uint256& hash, ChangeType status) {
73 if (status == CT_NEW) txid = hash;
74 }));
75 ConfirmSend();
76 bool invoked = QMetaObject::invokeMethod(&sendCoinsDialog, "sendButtonClicked", Q_ARG(bool, false));
77 assert(invoked);
78 return txid;
79}
80
82QModelIndex FindTx(const QAbstractItemModel& model, const uint256& txid)
83{
84 QString hash = QString::fromStdString(txid.ToString());
85 int rows = model.rowCount({});
86 for (int row = 0; row < rows; ++row) {
87 QModelIndex index = model.index(row, 0, {});
88 if (model.data(index, TransactionTableModel::TxHashRole) == hash) {
89 return index;
90 }
91 }
92 return {};
93}
94
96void BumpFee(TransactionView& view, const uint256& txid, bool expectDisabled, std::string expectError, bool cancel)
97{
98 QTableView* table = view.findChild<QTableView*>("transactionView");
99 QModelIndex index = FindTx(*table->selectionModel()->model(), txid);
100 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 QAction* action = view.findChild<QAction*>("bumpFeeAction");
105 table->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
106 action->setEnabled(expectDisabled);
107 table->customContextMenuRequested({});
108 QCOMPARE(action->isEnabled(), !expectDisabled);
109
110 action->setEnabled(true);
111 QString text;
112 if (expectError.empty()) {
113 ConfirmSend(&text, cancel);
114 } else {
115 ConfirmMessage(&text);
116 }
117 action->trigger();
118 QVERIFY(text.indexOf(QString::fromStdString(expectError)) != -1);
119}
120
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
134void TestGUI(interfaces::Node& node)
135{
136 // Set up wallet and chain with 105 blocks (5 mature blocks for spending).
138 for (int i = 0; i < 5; ++i) {
140 }
141 auto wallet_client = interfaces::MakeWalletClient(*test.m_node.chain, *Assert(test.m_node.args));
142 test.m_node.wallet_client = wallet_client.get();
143 node.setContext(&test.m_node);
144 const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(node.context()->chain.get(), "", CreateMockWalletDatabase());
145 wallet->LoadWallet();
146 wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
147 {
148 LOCK(wallet->cs_wallet);
149 wallet->SetupDescriptorScriptPubKeyMans();
150
151 // Add the coinbase key
152 FlatSigningProvider provider;
153 std::string error;
154 std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false);
155 assert(desc);
156 WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1);
157 if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false);
158 CTxDestination dest = GetDestinationForKey(test.coinbaseKey.GetPubKey(), wallet->m_default_address_type);
159 wallet->SetAddressBook(dest, "", "receive");
160 wallet->SetLastBlockProcessed(105, node.context()->chainman->ActiveChain().Tip()->GetBlockHash());
161 }
162 {
163 WalletRescanReserver reserver(*wallet);
164 reserver.reserve();
165 CWallet::ScanResult result = wallet->ScanForWalletTransactions(Params().GetConsensus().hashGenesisBlock, 0 /* block height */, {} /* max height */, reserver, true /* fUpdate */);
166 QCOMPARE(result.status, CWallet::ScanResult::SUCCESS);
167 QCOMPARE(result.last_scanned_block, node.context()->chainman->ActiveChain().Tip()->GetBlockHash());
168 QVERIFY(result.last_failed_block.IsNull());
169 }
170 wallet->SetBroadcastTransactions(true);
171
172 // Create widgets for sending coins and listing transactions.
173 std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other"));
174 SendCoinsDialog sendCoinsDialog(platformStyle.get());
175 TransactionView transactionView(platformStyle.get());
176 OptionsModel optionsModel;
177 ClientModel clientModel(node, &optionsModel);
178 WalletContext& context = *node.walletClient().context();
179 AddWallet(context, wallet);
180 WalletModel walletModel(interfaces::MakeWallet(context, wallet), clientModel, platformStyle.get());
181 RemoveWallet(context, wallet, /* load_on_start= */ std::nullopt);
182 sendCoinsDialog.setModel(&walletModel);
183 transactionView.setModel(&walletModel);
184
185 {
186 // Check balance in send dialog
187 QLabel* balanceLabel = sendCoinsDialog.findChild<QLabel*>("labelBalance");
188 QString balanceText = balanceLabel->text();
189 int unit = walletModel.getOptionsModel()->getDisplayUnit();
190 CAmount balance = walletModel.wallet().getBalance();
191 QString balanceComparison = BitcoinUnits::formatWithUnit(unit, balance, false, BitcoinUnits::SeparatorStyle::ALWAYS);
192 QCOMPARE(balanceText, balanceComparison);
193 }
194
195 // Send two transactions, and verify they are added to transaction list.
196 TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel();
197 QCOMPARE(transactionTableModel->rowCount({}), 105);
198 uint256 txid1 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 5 * COIN, false /* rbf */);
199 uint256 txid2 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 10 * COIN, true /* rbf */);
200 QCOMPARE(transactionTableModel->rowCount({}), 107);
201 QVERIFY(FindTx(*transactionTableModel, txid1).isValid());
202 QVERIFY(FindTx(*transactionTableModel, txid2).isValid());
203
204 // Call bumpfee. Test disabled, canceled, enabled, then failing cases.
205 BumpFee(transactionView, txid1, true /* expect disabled */, "not BIP 125 replaceable" /* expected error */, false /* cancel */);
206 BumpFee(transactionView, txid2, false /* expect disabled */, {} /* expected error */, true /* cancel */);
207 BumpFee(transactionView, txid2, false /* expect disabled */, {} /* expected error */, false /* cancel */);
208 BumpFee(transactionView, txid2, true /* expect disabled */, "already bumped" /* expected error */, false /* cancel */);
209
210 // Check current balance on OverviewPage
211 OverviewPage overviewPage(platformStyle.get());
212 overviewPage.setWalletModel(&walletModel);
213 QLabel* balanceLabel = overviewPage.findChild<QLabel*>("labelBalance");
214 QString balanceText = balanceLabel->text().trimmed();
215 int unit = walletModel.getOptionsModel()->getDisplayUnit();
216 CAmount balance = walletModel.wallet().getBalance();
217 QString balanceComparison = BitcoinUnits::formatWithUnit(unit, balance, false, BitcoinUnits::SeparatorStyle::ALWAYS);
218 QCOMPARE(balanceText, balanceComparison);
219
220 // Check Request Payment button
221 ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get());
222 receiveCoinsDialog.setModel(&walletModel);
223 RecentRequestsTableModel* requestTableModel = walletModel.getRecentRequestsTableModel();
224
225 // Label input
226 QLineEdit* labelInput = receiveCoinsDialog.findChild<QLineEdit*>("reqLabel");
227 labelInput->setText("TEST_LABEL_1");
228
229 // Amount input
230 BitcoinAmountField* amountInput = receiveCoinsDialog.findChild<BitcoinAmountField*>("reqAmount");
231 amountInput->setValue(1);
232
233 // Message input
234 QLineEdit* messageInput = receiveCoinsDialog.findChild<QLineEdit*>("reqMessage");
235 messageInput->setText("TEST_MESSAGE_1");
236 int initialRowCount = requestTableModel->rowCount({});
237 QPushButton* requestPaymentButton = receiveCoinsDialog.findChild<QPushButton*>("receiveButton");
238 requestPaymentButton->click();
239 QString address;
240 for (QWidget* widget : QApplication::topLevelWidgets()) {
241 if (widget->inherits("ReceiveRequestDialog")) {
242 ReceiveRequestDialog* receiveRequestDialog = qobject_cast<ReceiveRequestDialog*>(widget);
243 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("payment_header")->text(), QString("Payment information"));
244 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("uri_tag")->text(), QString("URI:"));
245 QString uri = receiveRequestDialog->QObject::findChild<QLabel*>("uri_content")->text();
246 QCOMPARE(uri.count("bitcoin:"), 2);
247 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("address_tag")->text(), QString("Address:"));
248 QVERIFY(address.isEmpty());
249 address = receiveRequestDialog->QObject::findChild<QLabel*>("address_content")->text();
250 QVERIFY(!address.isEmpty());
251
252 QCOMPARE(uri.count("amount=0.00000001"), 2);
253 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_tag")->text(), QString("Amount:"));
254 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_content")->text(), QString::fromStdString("0.00000001 " + CURRENCY_UNIT));
255
256 QCOMPARE(uri.count("label=TEST_LABEL_1"), 2);
257 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_tag")->text(), QString("Label:"));
258 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_content")->text(), QString("TEST_LABEL_1"));
259
260 QCOMPARE(uri.count("message=TEST_MESSAGE_1"), 2);
261 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_tag")->text(), QString("Message:"));
262 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_content")->text(), QString("TEST_MESSAGE_1"));
263 }
264 }
265
266 // Clear button
267 QPushButton* clearButton = receiveCoinsDialog.findChild<QPushButton*>("clearButton");
268 clearButton->click();
269 QCOMPARE(labelInput->text(), QString(""));
270 QCOMPARE(amountInput->value(), CAmount(0));
271 QCOMPARE(messageInput->text(), QString(""));
272
273 // Check addition to history
274 int currentRowCount = requestTableModel->rowCount({});
275 QCOMPARE(currentRowCount, initialRowCount+1);
276
277 // Check addition to wallet
278 std::vector<std::string> requests = walletModel.wallet().getAddressReceiveRequests();
279 QCOMPARE(requests.size(), size_t{1});
280 RecentRequestEntry entry;
281 CDataStream{MakeUCharSpan(requests[0]), SER_DISK, CLIENT_VERSION} >> entry;
282 QCOMPARE(entry.nVersion, int{1});
283 QCOMPARE(entry.id, int64_t{1});
284 QVERIFY(entry.date.isValid());
285 QCOMPARE(entry.recipient.address, address);
286 QCOMPARE(entry.recipient.label, QString{"TEST_LABEL_1"});
287 QCOMPARE(entry.recipient.amount, CAmount{1});
288 QCOMPARE(entry.recipient.message, QString{"TEST_MESSAGE_1"});
289 QCOMPARE(entry.recipient.sPaymentRequest, std::string{});
290 QCOMPARE(entry.recipient.authenticatedMerchant, QString{});
291
292 // Check Remove button
293 QTableView* table = receiveCoinsDialog.findChild<QTableView*>("recentRequestsView");
294 table->selectRow(currentRowCount-1);
295 QPushButton* removeRequestButton = receiveCoinsDialog.findChild<QPushButton*>("removeRequestButton");
296 removeRequestButton->click();
297 QCOMPARE(requestTableModel->rowCount({}), currentRowCount-1);
298
299 // Check removal from wallet
300 QCOMPARE(walletModel.wallet().getAddressReceiveRequests().size(), size_t{0});
301}
302
303} // namespace
304
306{
307#ifdef Q_OS_MAC
308 if (QApplication::platformName() == "minimal") {
309 // Disable for mac on "minimal" platform to avoid crashes inside the Qt
310 // framework when it tries to look up unimplemented cocoa functions,
311 // and fails to handle returned nulls
312 // (https://bugreports.qt.io/browse/QTBUG-49686).
313 QWARN("Skipping WalletTests on mac build with 'minimal' platform set due to Qt bugs. To run AppTests, invoke "
314 "with 'QT_QPA_PLATFORM=cocoa test_bitcoin-qt' on mac, or else use a linux or windows build.");
315 return;
316 }
317#endif
318 TestGUI(m_node);
319}
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.
#define Assert(val)
Identity function.
Definition: check.h:57
Widget for entering bitcoin amounts.
void setValue(const CAmount &value)
static QString formatWithUnit(int unit, const CAmount &amount, bool plussign=false, SeparatorStyle separators=SeparatorStyle::STANDARD)
Format as string (with unit)
Double ended buffer combining vector and stream-like interfaces.
Definition: streams.h:205
CPubKey GetPubKey() const
Compute the public key from a private key.
Definition: key.cpp:187
A CWallet maintains a set of transactions and balances, and provides the ability to create new transa...
Definition: wallet.h:229
Model for Bitcoin network client.
Definition: clientmodel.h:48
Interface from Qt to configuration data structure for Bitcoin client.
Definition: optionsmodel.h:39
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.
Descriptor with some wallet metadata.
Definition: walletutil.h:76
Interface to Bitcoin wallet from Qt view code.
Definition: walletmodel.h:52
RAII object to check and reserve a wallet rescan.
Definition: wallet.h:890
void walletTests()
interfaces::Node & m_node
Definition: wallettests.h:19
std::string ToString() const
Definition: uint256.cpp:64
bool IsNull() const
Definition: uint256.h:31
Top-level interface for a bitcoin node (bitcoind process).
Definition: node.h:55
256-bit opaque blob.
Definition: uint256.h:124
static const int CLIENT_VERSION
bitcoind-res.rc includes this file, but it cannot cope with real c++ code.
Definition: clientversion.h:33
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:14
std::string EncodeSecret(const CKey &key)
Definition: key_io.cpp:196
std::string EncodeDestination(const CTxDestination &dest)
Definition: key_io.cpp:256
std::unique_ptr< Wallet > MakeWallet(const std::shared_ptr< CWallet > &wallet)
Definition: dummywallet.cpp:62
std::unique_ptr< WalletClient > MakeWalletClient(Chain &chain, ArgsManager &args)
Return implementation of ChainClient interface for a wallet client.
Definition: dummywallet.cpp:67
CTxDestination GetDestinationForKey(const CPubKey &key, OutputType type)
Get a destination of the requested type (if possible) to the specified key.
Definition: outputtype.cpp:49
void ConfirmMessage(QString *text, int msec)
Press "Ok" button in message box dialog.
Definition: util.cpp:12
@ SER_DISK
Definition: serialize.h:139
constexpr auto MakeUCharSpan(V &&v) -> decltype(UCharSpanCast(MakeSpan(std::forward< V >(v))))
Like MakeSpan, but for (const) unsigned char member types only.
Definition: span.h:249
CScript GetScriptForRawPubKey(const CPubKey &pubKey)
Generate a P2PK script for the given pubkey.
Definition: standard.cpp:315
std::variant< CNoDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, WitnessUnknown > CTxDestination
A txout script template with a specific destination.
Definition: standard.h:157
NodeContext m_node
Definition: setup_common.h:78
uint256 last_failed_block
Height of the most recent block that could not be scanned due to read errors or pruning.
Definition: wallet.h:528
enum CWallet::ScanResult::@17 status
uint256 last_scanned_block
Hash and height of most recent block that was successfully scanned.
Definition: wallet.h:521
std::unique_ptr< interfaces::Chain > chain
Definition: context.h:50
ArgsManager * args
Definition: context.h:49
interfaces::WalletClient * wallet_client
Reference to chain client that should used to load or create wallets opened by the gui.
Definition: context.h:55
Testing fixture that pre-creates a 100-block REGTEST-mode block chain.
Definition: setup_common.h:116
CBlock CreateAndProcessBlock(const std::vector< CMutableTransaction > &txns, const CScript &scriptPubKey, CChainState *chainstate=nullptr)
Create a new block with just given transactions, coinbase paying to scriptPubKey, and try to add it t...
WalletContext struct containing references to state shared between CWallet instances,...
Definition: context.h:34
#define LOCK(cs)
Definition: sync.h:226
bool error(const char *fmt, const Args &... args)
Definition: system.h:49
ChangeType
General change type (added, updated, removed).
Definition: ui_change_type.h:9
assert(!tx.IsCoinBase())
bool RemoveWallet(WalletContext &context, const std::shared_ptr< CWallet > &wallet, std::optional< bool > load_on_start, std::vector< bilingual_str > &warnings)
Definition: wallet.cpp:117
bool AddWallet(WalletContext &context, const std::shared_ptr< CWallet > &wallet)
Definition: wallet.cpp:105
std::unique_ptr< WalletDatabase > CreateMockWalletDatabase()
Return object for accessing temporary in-memory database.
Definition: walletdb.cpp:1189
@ WALLET_FLAG_DESCRIPTORS
Indicate that this wallet supports DescriptorScriptPubKeyMan.
Definition: walletutil.h:65