sendcoinsdialog.cpp raw
1 // Copyright (c) 2011-present 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 <bitcoin-build-config.h> // IWYU pragma: keep
6
7 #include <qt/sendcoinsdialog.h>
8 #include <qt/forms/ui_sendcoinsdialog.h>
9
10 #include <qt/addresstablemodel.h>
11 #include <qt/bitcoinunits.h>
12 #include <qt/clientmodel.h>
13 #include <qt/coincontroldialog.h>
14 #include <qt/guiutil.h>
15 #include <qt/optionsmodel.h>
16 #include <qt/platformstyle.h>
17 #include <qt/sendcoinsentry.h>
18
19 #include <chainparams.h>
20 #include <interfaces/node.h>
21 #include <key_io.h>
22 #include <node/interface_ui.h>
23 #include <node/types.h>
24 #include <policy/fees/block_policy_estimator.h>
25 #include <txmempool.h>
26 #include <validation.h>
27 #include <wallet/coincontrol.h>
28 #include <wallet/fees.h>
29 #include <wallet/wallet.h>
30
31 #include <array>
32 #include <chrono>
33 #include <fstream>
34 #include <memory>
35
36 #include <QFontMetrics>
37 #include <QScrollBar>
38 #include <QSettings>
39 #include <QTextDocument>
40
41 using common::PSBTError;
42 using wallet::CCoinControl;
43
44 static constexpr std::array confTargets{2, 4, 6, 12, 24, 48, 144, 504, 1008};
45 int getConfTargetForIndex(int index) {
46 if (index+1 > static_cast<int>(confTargets.size())) {
47 return confTargets.back();
48 }
49 if (index < 0) {
50 return confTargets[0];
51 }
52 return confTargets[index];
53 }
54 int getIndexForConfTarget(int target) {
55 for (unsigned int i = 0; i < confTargets.size(); i++) {
56 if (confTargets[i] >= target) {
57 return i;
58 }
59 }
60 return confTargets.size() - 1;
61 }
62
63 SendCoinsDialog::SendCoinsDialog(const PlatformStyle *_platformStyle, QWidget *parent) :
64 QDialog(parent, GUIUtil::dialog_flags),
65 ui(new Ui::SendCoinsDialog),
66 m_coin_control(new CCoinControl),
67 platformStyle(_platformStyle)
68 {
69 ui->setupUi(this);
70
71 if (!_platformStyle->getImagesOnButtons()) {
72 ui->addButton->setIcon(QIcon());
73 ui->clearButton->setIcon(QIcon());
74 ui->sendButton->setIcon(QIcon());
75 } else {
76 ui->addButton->setIcon(_platformStyle->SingleColorIcon(":/icons/add"));
77 ui->clearButton->setIcon(_platformStyle->SingleColorIcon(":/icons/remove"));
78 ui->sendButton->setIcon(_platformStyle->SingleColorIcon(":/icons/send"));
79 }
80
81 GUIUtil::setupAddressWidget(ui->lineEditCoinControlChange, this);
82
83 addEntry();
84
85 connect(ui->addButton, &QPushButton::clicked, this, &SendCoinsDialog::addEntry);
86 connect(ui->clearButton, &QPushButton::clicked, this, &SendCoinsDialog::clear);
87
88 // Coin Control
89 connect(ui->pushButtonCoinControl, &QPushButton::clicked, this, &SendCoinsDialog::coinControlButtonClicked);
90 #if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0))
91 connect(ui->checkBoxCoinControlChange, &QCheckBox::checkStateChanged, this, &SendCoinsDialog::coinControlChangeChecked);
92 #else
93 connect(ui->checkBoxCoinControlChange, &QCheckBox::stateChanged, this, &SendCoinsDialog::coinControlChangeChecked);
94 #endif
95 connect(ui->lineEditCoinControlChange, &QValidatedLineEdit::textEdited, this, &SendCoinsDialog::coinControlChangeEdited);
96
97 // Coin Control: clipboard actions
98 QAction *clipboardQuantityAction = new QAction(tr("Copy quantity"), this);
99 QAction *clipboardAmountAction = new QAction(tr("Copy amount"), this);
100 QAction *clipboardFeeAction = new QAction(tr("Copy fee"), this);
101 QAction *clipboardAfterFeeAction = new QAction(tr("Copy after fee"), this);
102 QAction *clipboardBytesAction = new QAction(tr("Copy bytes"), this);
103 QAction *clipboardChangeAction = new QAction(tr("Copy change"), this);
104 connect(clipboardQuantityAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardQuantity);
105 connect(clipboardAmountAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardAmount);
106 connect(clipboardFeeAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardFee);
107 connect(clipboardAfterFeeAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardAfterFee);
108 connect(clipboardBytesAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardBytes);
109 connect(clipboardChangeAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardChange);
110 ui->labelCoinControlQuantity->addAction(clipboardQuantityAction);
111 ui->labelCoinControlAmount->addAction(clipboardAmountAction);
112 ui->labelCoinControlFee->addAction(clipboardFeeAction);
113 ui->labelCoinControlAfterFee->addAction(clipboardAfterFeeAction);
114 ui->labelCoinControlBytes->addAction(clipboardBytesAction);
115 ui->labelCoinControlChange->addAction(clipboardChangeAction);
116
117 // init transaction fee section
118 QSettings settings;
119 if (!settings.contains("fFeeSectionMinimized"))
120 settings.setValue("fFeeSectionMinimized", true);
121 if (!settings.contains("nFeeRadio") && settings.contains("nTransactionFee") && settings.value("nTransactionFee").toLongLong() > 0) // compatibility
122 settings.setValue("nFeeRadio", 1); // custom
123 if (!settings.contains("nFeeRadio"))
124 settings.setValue("nFeeRadio", 0); // recommended
125 if (!settings.contains("nSmartFeeSliderPosition"))
126 settings.setValue("nSmartFeeSliderPosition", 0);
127 ui->groupFee->setId(ui->radioSmartFee, 0);
128 ui->groupFee->setId(ui->radioCustomFee, 1);
129 ui->groupFee->button((int)std::max(0, std::min(1, settings.value("nFeeRadio").toInt())))->setChecked(true);
130 ui->customFee->SetAllowEmpty(false);
131 ui->customFee->setValue(settings.value("nTransactionFee").toLongLong());
132 minimizeFeeSection(settings.value("fFeeSectionMinimized").toBool());
133
134 GUIUtil::ExceptionSafeConnect(ui->sendButton, &QPushButton::clicked, this, &SendCoinsDialog::sendButtonClicked);
135 }
136
137 void SendCoinsDialog::setClientModel(ClientModel *_clientModel)
138 {
139 this->clientModel = _clientModel;
140
141 if (_clientModel) {
142 connect(_clientModel, &ClientModel::numBlocksChanged, this, &SendCoinsDialog::updateNumberOfBlocks);
143 }
144 }
145
146 void SendCoinsDialog::setModel(WalletModel *_model)
147 {
148 this->model = _model;
149
150 if(_model && _model->getOptionsModel())
151 {
152 for(int i = 0; i < ui->entries->count(); ++i)
153 {
154 SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
155 if(entry)
156 {
157 entry->setModel(_model);
158 }
159 }
160
161 connect(_model, &WalletModel::balanceChanged, this, &SendCoinsDialog::setBalance);
162 connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::refreshBalance);
163 refreshBalance();
164
165 // Coin Control
166 connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::coinControlUpdateLabels);
167 connect(_model->getOptionsModel(), &OptionsModel::coinControlFeaturesChanged, this, &SendCoinsDialog::coinControlFeatureChanged);
168 ui->frameCoinControl->setVisible(_model->getOptionsModel()->getCoinControlFeatures());
169 coinControlUpdateLabels();
170
171 // fee section
172 for (const int n : confTargets) {
173 ui->confTargetSelector->addItem(tr("%1 (%2 blocks)").arg(GUIUtil::formatNiceTimeOffset(n*Params().GetConsensus().nPowTargetSpacing)).arg(n));
174 }
175 connect(ui->confTargetSelector, qOverload<int>(&QComboBox::currentIndexChanged), this, &SendCoinsDialog::updateSmartFeeLabel);
176 connect(ui->confTargetSelector, qOverload<int>(&QComboBox::currentIndexChanged), this, &SendCoinsDialog::coinControlUpdateLabels);
177
178 connect(ui->groupFee, &QButtonGroup::idClicked, this, &SendCoinsDialog::updateFeeSectionControls);
179 connect(ui->groupFee, &QButtonGroup::idClicked, this, &SendCoinsDialog::coinControlUpdateLabels);
180
181 connect(ui->customFee, &BitcoinAmountField::valueChanged, this, &SendCoinsDialog::coinControlUpdateLabels);
182 CAmount requiredFee = model->wallet().getRequiredFee(1000);
183 ui->customFee->SetMinValue(requiredFee);
184 if (ui->customFee->value() < requiredFee) {
185 ui->customFee->setValue(requiredFee);
186 }
187 ui->customFee->setSingleStep(requiredFee);
188 updateFeeSectionControls();
189 updateSmartFeeLabel();
190
191 if (model->wallet().hasExternalSigner()) {
192 //: "device" usually means a hardware wallet.
193 ui->sendButton->setText(tr("Sign on device"));
194 if (model->getOptionsModel()->hasSigner()) {
195 ui->sendButton->setEnabled(true);
196 ui->sendButton->setToolTip(tr("Connect your hardware wallet first."));
197 } else {
198 ui->sendButton->setEnabled(false);
199 //: "External signer" means using devices such as hardware wallets.
200 ui->sendButton->setToolTip(tr("Set external signer script path in Options -> Wallet"));
201 }
202 } else if (model->wallet().privateKeysDisabled()) {
203 ui->sendButton->setText(tr("Cr&eate Unsigned"));
204 ui->sendButton->setToolTip(tr("Creates a Partially Signed Bitcoin Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(CLIENT_NAME));
205 }
206
207 // set the smartfee-sliders default value (wallets default conf.target or last stored value)
208 QSettings settings;
209 if (settings.value("nSmartFeeSliderPosition").toInt() != 0) {
210 // migrate nSmartFeeSliderPosition to nConfTarget
211 // nConfTarget is available since 0.15 (replaced nSmartFeeSliderPosition)
212 int nConfirmTarget = 25 - settings.value("nSmartFeeSliderPosition").toInt(); // 25 == old slider range
213 settings.setValue("nConfTarget", nConfirmTarget);
214 settings.remove("nSmartFeeSliderPosition");
215 }
216 if (settings.value("nConfTarget").toInt() == 0)
217 ui->confTargetSelector->setCurrentIndex(getIndexForConfTarget(model->wallet().getConfirmTarget()));
218 else
219 ui->confTargetSelector->setCurrentIndex(getIndexForConfTarget(settings.value("nConfTarget").toInt()));
220 }
221 }
222
223 SendCoinsDialog::~SendCoinsDialog()
224 {
225 QSettings settings;
226 settings.setValue("fFeeSectionMinimized", fFeeMinimized);
227 settings.setValue("nFeeRadio", ui->groupFee->checkedId());
228 settings.setValue("nConfTarget", getConfTargetForIndex(ui->confTargetSelector->currentIndex()));
229 settings.setValue("nTransactionFee", (qint64)ui->customFee->value());
230
231 delete ui;
232 }
233
234 bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informative_text, QString& detailed_text)
235 {
236 QList<SendCoinsRecipient> recipients;
237 bool valid = true;
238
239 for(int i = 0; i < ui->entries->count(); ++i)
240 {
241 SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
242 if(entry)
243 {
244 if(entry->validate(model->node()))
245 {
246 recipients.append(entry->getValue());
247 }
248 else if (valid)
249 {
250 ui->scrollArea->ensureWidgetVisible(entry);
251 valid = false;
252 }
253 }
254 }
255
256 if(!valid || recipients.isEmpty())
257 {
258 return false;
259 }
260
261 fNewRecipientAllowed = false;
262 WalletModel::UnlockContext ctx(model->requestUnlock());
263 if(!ctx.isValid())
264 {
265 // Unlock wallet was cancelled
266 fNewRecipientAllowed = true;
267 return false;
268 }
269
270 // prepare transaction for getting txFee earlier
271 m_current_transaction = std::make_unique<WalletModelTransaction>(recipients);
272 WalletModel::SendCoinsReturn prepareStatus;
273
274 updateCoinControlState();
275
276 CCoinControl coin_control = *m_coin_control;
277 coin_control.m_allow_other_inputs = !coin_control.HasSelected(); // future, could introduce a checkbox to customize this value.
278 prepareStatus = model->prepareTransaction(*m_current_transaction, coin_control);
279
280 // process prepareStatus and on error generate message shown to user
281 processSendCoinsReturn(prepareStatus,
282 BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), m_current_transaction->getTransactionFee()));
283
284 if(prepareStatus.status != WalletModel::OK) {
285 fNewRecipientAllowed = true;
286 return false;
287 }
288
289 CAmount txFee = m_current_transaction->getTransactionFee();
290 QStringList formatted;
291 for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients())
292 {
293 // generate amount string with wallet name in case of multiwallet
294 QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
295 if (model->isMultiwallet()) {
296 amount = tr("%1 from wallet '%2'").arg(amount, GUIUtil::HtmlEscape(model->getWalletName()));
297 }
298
299 // generate address string
300 QString address = rcp.address;
301
302 QString recipientElement;
303
304 {
305 if(rcp.label.length() > 0) // label with address
306 {
307 recipientElement.append(tr("%1 to '%2'").arg(amount, GUIUtil::HtmlEscape(rcp.label)));
308 recipientElement.append(QString(" (%1)").arg(address));
309 }
310 else // just address
311 {
312 recipientElement.append(tr("%1 to %2").arg(amount, address));
313 }
314 }
315 formatted.append(recipientElement);
316 }
317
318 /*: Message displayed when attempting to create a transaction. Cautionary text to prompt the user to verify
319 that the displayed transaction details represent the transaction the user intends to create. */
320 question_string.append(tr("Do you want to create this transaction?"));
321 question_string.append("<br /><span style='font-size:10pt;'>");
322 if (model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner()) {
323 /*: Text to inform a user attempting to create a transaction of their current options. At this stage,
324 a user can only create a PSBT. This string is displayed when private keys are disabled and an external
325 signer is not available. */
326 question_string.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Bitcoin Transaction (PSBT) which you can save or copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(CLIENT_NAME));
327 } else if (model->getOptionsModel()->getEnablePSBTControls()) {
328 /*: Text to inform a user attempting to create a transaction of their current options. At this stage,
329 a user can send their transaction or create a PSBT. This string is displayed when both private keys
330 and PSBT controls are enabled. */
331 question_string.append(tr("Please, review your transaction. You can create and send this transaction or create a Partially Signed Bitcoin Transaction (PSBT), which you can save or copy and then sign with, e.g., an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(CLIENT_NAME));
332 } else {
333 /*: Text to prompt a user to review the details of the transaction they are attempting to send. */
334 question_string.append(tr("Please, review your transaction."));
335 }
336 question_string.append("</span>%1");
337
338 if(txFee > 0)
339 {
340 // append fee string if a fee is required
341 question_string.append("<hr /><b>");
342 question_string.append(tr("Transaction fee"));
343 question_string.append("</b>");
344
345 // append transaction size
346 //: When reviewing a newly created PSBT (via Send flow), the transaction fee is shown, with "virtual size" of the transaction displayed for context
347 question_string.append(" (" + tr("%1 kvB", "PSBT transaction creation").arg((double)m_current_transaction->getTransactionSize() / 1000, 0, 'g', 3) + "): ");
348
349 // append transaction fee value
350 question_string.append("<span style='color:#aa0000; font-weight:bold;'>");
351 question_string.append(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), txFee));
352 question_string.append("</span><br />");
353 }
354
355 // append RBF message
356 question_string.append("<span style='font-size:10pt; font-weight:normal;'>");
357 question_string.append(tr("You can increase the fee later."));
358
359 // add total amount in all subdivision units
360 question_string.append("<hr />");
361 CAmount totalAmount = m_current_transaction->getTotalTransactionAmount() + txFee;
362 QStringList alternativeUnits;
363 for (const BitcoinUnit u : BitcoinUnits::availableUnits()) {
364 if(u != model->getOptionsModel()->getDisplayUnit())
365 alternativeUnits.append(BitcoinUnits::formatHtmlWithUnit(u, totalAmount));
366 }
367 question_string.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
368 .arg(BitcoinUnits::formatHtmlWithUnit(model->getOptionsModel()->getDisplayUnit(), totalAmount)));
369 question_string.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
370 .arg(alternativeUnits.join(" " + tr("or") + " ")));
371
372 if (formatted.size() > 1) {
373 question_string = question_string.arg("");
374 informative_text = tr("To review recipient list click \"Show Details…\"");
375 detailed_text = formatted.join("\n\n");
376 } else {
377 question_string = question_string.arg("<br /><br />" + formatted.at(0));
378 }
379
380 return true;
381 }
382
383 void SendCoinsDialog::presentPSBT(PartiallySignedTransaction& psbtx)
384 {
385 // Serialize the PSBT
386 DataStream ssTx{};
387 ssTx << psbtx;
388 GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
389 QMessageBox msgBox(this);
390 //: Caption of "PSBT has been copied" messagebox
391 msgBox.setText(tr("Unsigned Transaction", "PSBT copied"));
392 msgBox.setInformativeText(tr("The PSBT has been copied to the clipboard. You can also save it."));
393 msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard);
394 msgBox.setDefaultButton(QMessageBox::Discard);
395 msgBox.setObjectName("psbt_copied_message");
396 switch (msgBox.exec()) {
397 case QMessageBox::Save: {
398 QString selectedFilter;
399 QString fileNameSuggestion = "";
400 bool first = true;
401 for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients()) {
402 if (!first) {
403 fileNameSuggestion.append(" - ");
404 }
405 QString labelOrAddress = rcp.label.isEmpty() ? rcp.address : rcp.label;
406 QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
407 fileNameSuggestion.append(labelOrAddress + "-" + amount);
408 first = false;
409 }
410 fileNameSuggestion.append(".psbt");
411 QString filename = GUIUtil::getSaveFileName(this,
412 tr("Save Transaction Data"), fileNameSuggestion,
413 //: Expanded name of the binary PSBT file format. See: BIP 174.
414 tr("Partially Signed Transaction (Binary)") + QLatin1String(" (*.psbt)"), &selectedFilter);
415 if (filename.isEmpty()) {
416 return;
417 }
418 std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
419 out << ssTx.str();
420 out.close();
421 //: Popup message when a PSBT has been saved to a file
422 Q_EMIT message(tr("PSBT saved"), tr("PSBT saved to disk"), CClientUIInterface::MSG_INFORMATION);
423 break;
424 }
425 case QMessageBox::Discard:
426 break;
427 default:
428 assert(false);
429 } // msgBox.exec()
430 }
431
432 bool SendCoinsDialog::signWithExternalSigner(PartiallySignedTransaction& psbtx, CMutableTransaction& mtx, bool& complete) {
433 std::optional<PSBTError> err;
434 try {
435 err = model->wallet().fillPSBT({.sign = true, .bip32_derivs = true}, /*n_signed=*/nullptr, psbtx, complete);
436 } catch (const std::runtime_error& e) {
437 QMessageBox::critical(nullptr, tr("Sign failed"), e.what());
438 return false;
439 }
440 if (err == PSBTError::EXTERNAL_SIGNER_NOT_FOUND) {
441 //: "External signer" means using devices such as hardware wallets.
442 const QString msg = tr("External signer not found");
443 QMessageBox::critical(nullptr, msg, msg);
444 return false;
445 }
446 if (err == PSBTError::EXTERNAL_SIGNER_FAILED) {
447 //: "External signer" means using devices such as hardware wallets.
448 const QString msg = tr("External signer failure");
449 QMessageBox::critical(nullptr, msg, msg);
450 return false;
451 }
452 if (err) {
453 qWarning() << "Failed to sign PSBT";
454 processSendCoinsReturn(WalletModel::TransactionCreationFailed);
455 return false;
456 }
457 // fillPSBT does not always properly finalize
458 complete = FinalizeAndExtractPSBT(psbtx, mtx);
459 return true;
460 }
461
462 void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
463 {
464 if(!model || !model->getOptionsModel())
465 return;
466
467 QString question_string, informative_text, detailed_text;
468 if (!PrepareSendText(question_string, informative_text, detailed_text)) return;
469 assert(m_current_transaction);
470
471 const QString confirmation = tr("Confirm send coins");
472 const bool enable_send{!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner()};
473 const bool always_show_unsigned{model->getOptionsModel()->getEnablePSBTControls()};
474 auto confirmationDialog = new SendConfirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, enable_send, always_show_unsigned, this);
475 confirmationDialog->setAttribute(Qt::WA_DeleteOnClose);
476 // TODO: Replace QDialog::exec() with safer QDialog::show().
477 const auto retval = static_cast<QMessageBox::StandardButton>(confirmationDialog->exec());
478
479 if(retval != QMessageBox::Yes && retval != QMessageBox::Save)
480 {
481 fNewRecipientAllowed = true;
482 return;
483 }
484
485 bool send_failure = false;
486 if (retval == QMessageBox::Save) {
487 // "Create Unsigned" clicked
488 CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
489 PartiallySignedTransaction psbtx(mtx);
490 bool complete = false;
491 // Fill without signing
492 const auto err{model->wallet().fillPSBT({.sign = false, .bip32_derivs = true}, /*n_signed=*/nullptr, psbtx, complete)};
493 assert(!complete);
494 assert(!err);
495
496 // Copy PSBT to clipboard and offer to save
497 presentPSBT(psbtx);
498 } else {
499 // "Send" clicked
500 assert(!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner());
501 bool broadcast = true;
502 if (model->wallet().hasExternalSigner()) {
503 CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
504 PartiallySignedTransaction psbtx(mtx);
505 bool complete = false;
506 // Always fill without signing first. This prevents an external signer
507 // from being called prematurely and is not expensive.
508 const auto err{model->wallet().fillPSBT({.sign = false, .bip32_derivs = true}, /*n_signed=*/nullptr, psbtx, complete)};
509 assert(!complete);
510 assert(!err);
511 send_failure = !signWithExternalSigner(psbtx, mtx, complete);
512 // Don't broadcast when user rejects it on the device or there's a failure:
513 broadcast = complete && !send_failure;
514 if (!send_failure) {
515 // A transaction signed with an external signer is not always complete,
516 // e.g. in a multisig wallet.
517 if (complete) {
518 // Prepare transaction for broadcast transaction if complete
519 const CTransactionRef tx = MakeTransactionRef(mtx);
520 m_current_transaction->setWtx(tx);
521 } else {
522 presentPSBT(psbtx);
523 }
524 }
525 }
526
527 // Broadcast the transaction, unless an external signer was used and it
528 // failed, or more signatures are needed.
529 if (broadcast) {
530 // now send the prepared transaction
531 model->sendCoins(*m_current_transaction);
532 Q_EMIT coinsSent(m_current_transaction->getWtx()->GetHash());
533 }
534 }
535 if (!send_failure) {
536 accept();
537 m_coin_control->UnSelectAll();
538 coinControlUpdateLabels();
539 }
540 fNewRecipientAllowed = true;
541 m_current_transaction.reset();
542 }
543
544 void SendCoinsDialog::clear()
545 {
546 m_current_transaction.reset();
547
548 // Clear coin control settings
549 m_coin_control->UnSelectAll();
550 ui->checkBoxCoinControlChange->setChecked(false);
551 ui->lineEditCoinControlChange->clear();
552 coinControlUpdateLabels();
553
554 // Remove entries until only one left
555 while(ui->entries->count())
556 {
557 ui->entries->takeAt(0)->widget()->deleteLater();
558 }
559 addEntry();
560
561 updateTabsAndLabels();
562 }
563
564 void SendCoinsDialog::reject()
565 {
566 clear();
567 }
568
569 void SendCoinsDialog::accept()
570 {
571 clear();
572 }
573
574 SendCoinsEntry *SendCoinsDialog::addEntry()
575 {
576 SendCoinsEntry *entry = new SendCoinsEntry(platformStyle, this);
577 entry->setModel(model);
578 ui->entries->addWidget(entry);
579 connect(entry, &SendCoinsEntry::removeEntry, this, &SendCoinsDialog::removeEntry);
580 connect(entry, &SendCoinsEntry::useAvailableBalance, this, &SendCoinsDialog::useAvailableBalance);
581 connect(entry, &SendCoinsEntry::payAmountChanged, this, &SendCoinsDialog::coinControlUpdateLabels);
582 connect(entry, &SendCoinsEntry::subtractFeeFromAmountChanged, this, &SendCoinsDialog::coinControlUpdateLabels);
583
584 // Focus the field, so that entry can start immediately
585 entry->clear();
586 entry->setFocus();
587 ui->scrollAreaWidgetContents->resize(ui->scrollAreaWidgetContents->sizeHint());
588
589 // Scroll to the newly added entry on a QueuedConnection because Qt doesn't
590 // adjust the scroll area and scrollbar immediately when the widget is added.
591 // Invoking on a DirectConnection will only scroll to the second-to-last entry.
592 QMetaObject::invokeMethod(ui->scrollArea, [this] {
593 if (ui->scrollArea->verticalScrollBar()) {
594 ui->scrollArea->verticalScrollBar()->setValue(ui->scrollArea->verticalScrollBar()->maximum());
595 }
596 }, Qt::QueuedConnection);
597
598 updateTabsAndLabels();
599 return entry;
600 }
601
602 void SendCoinsDialog::updateTabsAndLabels()
603 {
604 setupTabChain(nullptr);
605 coinControlUpdateLabels();
606 }
607
608 void SendCoinsDialog::removeEntry(SendCoinsEntry* entry)
609 {
610 entry->hide();
611
612 // If the last entry is about to be removed add an empty one
613 if (ui->entries->count() == 1)
614 addEntry();
615
616 entry->deleteLater();
617
618 updateTabsAndLabels();
619 }
620
621 QWidget *SendCoinsDialog::setupTabChain(QWidget *prev)
622 {
623 for(int i = 0; i < ui->entries->count(); ++i)
624 {
625 SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
626 if(entry)
627 {
628 prev = entry->setupTabChain(prev);
629 }
630 }
631 QWidget::setTabOrder(prev, ui->sendButton);
632 QWidget::setTabOrder(ui->sendButton, ui->clearButton);
633 QWidget::setTabOrder(ui->clearButton, ui->addButton);
634 return ui->addButton;
635 }
636
637 void SendCoinsDialog::setAddress(const QString &address)
638 {
639 SendCoinsEntry *entry = nullptr;
640 // Replace the first entry if it is still unused
641 if(ui->entries->count() == 1)
642 {
643 SendCoinsEntry *first = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(0)->widget());
644 if(first->isClear())
645 {
646 entry = first;
647 }
648 }
649 if(!entry)
650 {
651 entry = addEntry();
652 }
653
654 entry->setAddress(address);
655 }
656
657 void SendCoinsDialog::pasteEntry(const SendCoinsRecipient &rv)
658 {
659 if(!fNewRecipientAllowed)
660 return;
661
662 SendCoinsEntry *entry = nullptr;
663 // Replace the first entry if it is still unused
664 if(ui->entries->count() == 1)
665 {
666 SendCoinsEntry *first = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(0)->widget());
667 if(first->isClear())
668 {
669 entry = first;
670 }
671 }
672 if(!entry)
673 {
674 entry = addEntry();
675 }
676
677 entry->setValue(rv);
678 updateTabsAndLabels();
679 }
680
681 bool SendCoinsDialog::handlePaymentRequest(const SendCoinsRecipient &rv)
682 {
683 // Just paste the entry, all pre-checks
684 // are done in paymentserver.cpp.
685 pasteEntry(rv);
686 return true;
687 }
688
689 void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances)
690 {
691 if(model && model->getOptionsModel())
692 {
693 CAmount balance = balances.balance;
694 if (model->wallet().hasExternalSigner()) {
695 ui->labelBalanceName->setText(tr("External balance:"));
696 }
697 ui->labelBalance->setText(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), balance));
698 }
699 }
700
701 void SendCoinsDialog::refreshBalance()
702 {
703 setBalance(model->getCachedBalance());
704 ui->customFee->setDisplayUnit(model->getOptionsModel()->getDisplayUnit());
705 updateSmartFeeLabel();
706 }
707
708 void SendCoinsDialog::processSendCoinsReturn(const WalletModel::SendCoinsReturn &sendCoinsReturn, const QString &msgArg)
709 {
710 QPair<QString, CClientUIInterface::MessageBoxFlags> msgParams;
711 // Default to a warning message, override if error message is needed
712 msgParams.second = CClientUIInterface::MSG_WARNING;
713
714 // This comment is specific to SendCoinsDialog usage of WalletModel::SendCoinsReturn.
715 // All status values are used only in WalletModel::prepareTransaction()
716 switch(sendCoinsReturn.status)
717 {
718 case WalletModel::InvalidAddress:
719 msgParams.first = tr("The recipient address is not valid. Please recheck.");
720 break;
721 case WalletModel::InvalidAmount:
722 msgParams.first = tr("The amount to pay must be larger than 0.");
723 break;
724 case WalletModel::AmountExceedsBalance:
725 msgParams.first = tr("The amount exceeds your balance.");
726 break;
727 case WalletModel::DuplicateAddress:
728 msgParams.first = tr("Duplicate address found: addresses should only be used once each.");
729 break;
730 case WalletModel::TransactionCreationFailed:
731 msgParams.first = tr("Transaction creation failed!");
732 msgParams.second = CClientUIInterface::MSG_ERROR;
733 break;
734 case WalletModel::AbsurdFee:
735 msgParams.first = tr("A fee higher than %1 is considered an absurdly high fee.").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), model->wallet().getDefaultMaxTxFee()));
736 break;
737 case WalletModel::OK:
738 return;
739 } // no default case, so the compiler can warn about missing cases
740 Q_EMIT message(tr("Send Coins"), msgParams.first, msgParams.second);
741 }
742
743 void SendCoinsDialog::minimizeFeeSection(bool fMinimize)
744 {
745 ui->labelFeeMinimized->setVisible(fMinimize);
746 ui->buttonChooseFee ->setVisible(fMinimize);
747 ui->buttonMinimizeFee->setVisible(!fMinimize);
748 ui->frameFeeSelection->setVisible(!fMinimize);
749 ui->horizontalLayoutSmartFee->setContentsMargins(0, (fMinimize ? 0 : 6), 0, 0);
750 fFeeMinimized = fMinimize;
751 }
752
753 void SendCoinsDialog::on_buttonChooseFee_clicked()
754 {
755 minimizeFeeSection(false);
756 }
757
758 void SendCoinsDialog::on_buttonMinimizeFee_clicked()
759 {
760 updateFeeMinimizedLabel();
761 minimizeFeeSection(true);
762 }
763
764 void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry)
765 {
766 // Same behavior as send: if we have selected coins, only obtain their available balance.
767 // Copy to avoid modifying the member's data.
768 CCoinControl coin_control = *m_coin_control;
769 coin_control.m_allow_other_inputs = !coin_control.HasSelected();
770
771 // Calculate available amount to send.
772 CAmount amount = model->getAvailableBalance(&coin_control);
773 for (int i = 0; i < ui->entries->count(); ++i) {
774 SendCoinsEntry* e = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
775 if (e && !e->isHidden() && e != entry) {
776 amount -= e->getValue().amount;
777 }
778 }
779
780 if (amount > 0) {
781 entry->checkSubtractFeeFromAmount();
782 entry->setAmount(amount);
783 } else {
784 entry->setAmount(0);
785 }
786 }
787
788 void SendCoinsDialog::updateFeeSectionControls()
789 {
790 ui->confTargetSelector ->setEnabled(ui->radioSmartFee->isChecked());
791 ui->labelSmartFee ->setEnabled(ui->radioSmartFee->isChecked());
792 ui->labelSmartFee2 ->setEnabled(ui->radioSmartFee->isChecked());
793 ui->labelSmartFee3 ->setEnabled(ui->radioSmartFee->isChecked());
794 ui->labelFeeEstimation ->setEnabled(ui->radioSmartFee->isChecked());
795 ui->labelCustomFeeWarning ->setEnabled(ui->radioCustomFee->isChecked());
796 ui->labelCustomPerKilobyte ->setEnabled(ui->radioCustomFee->isChecked());
797 ui->customFee ->setEnabled(ui->radioCustomFee->isChecked());
798 }
799
800 void SendCoinsDialog::updateFeeMinimizedLabel()
801 {
802 if(!model || !model->getOptionsModel())
803 return;
804
805 if (ui->radioSmartFee->isChecked())
806 ui->labelFeeMinimized->setText(ui->labelSmartFee->text());
807 else {
808 ui->labelFeeMinimized->setText(tr("%1/kvB").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), ui->customFee->value())));
809 }
810 }
811
812 void SendCoinsDialog::updateCoinControlState()
813 {
814 if (ui->radioCustomFee->isChecked()) {
815 m_coin_control->m_feerate = CFeeRate(ui->customFee->value());
816 } else {
817 m_coin_control->m_feerate.reset();
818 }
819 // Avoid using global defaults when sending money from the GUI
820 // Either custom fee will be used or if not selected, the confirmation target from dropdown box
821 m_coin_control->m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex());
822 }
823
824 void SendCoinsDialog::updateNumberOfBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state) {
825 // During shutdown, clientModel will be nullptr. Attempting to update views at this point may cause a crash
826 // due to accessing backend models that might no longer exist.
827 if (!clientModel) return;
828 // Process event
829 if (sync_state == SynchronizationState::POST_INIT) {
830 updateSmartFeeLabel();
831 }
832 }
833
834 void SendCoinsDialog::updateSmartFeeLabel()
835 {
836 if(!model || !model->getOptionsModel())
837 return;
838 updateCoinControlState();
839 m_coin_control->m_feerate.reset(); // Explicitly use only fee estimation rate for smart fee labels
840 int returned_target;
841 FeeReason reason;
842 CFeeRate feeRate = CFeeRate(model->wallet().getMinimumFee(1000, *m_coin_control, &returned_target, &reason));
843
844 ui->labelSmartFee->setText(tr("%1/kvB").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), feeRate.GetFeePerK())));
845
846 if (reason == FeeReason::FALLBACK) {
847 ui->labelSmartFee2->show(); // (Smart fee not initialized yet. This usually takes a few blocks...)
848 ui->labelFeeEstimation->setText("");
849 ui->fallbackFeeWarningLabel->setVisible(true);
850 int lightness = ui->fallbackFeeWarningLabel->palette().color(QPalette::WindowText).lightness();
851 QColor warning_colour(255 - (lightness / 5), 176 - (lightness / 3), 48 - (lightness / 14));
852 ui->fallbackFeeWarningLabel->setStyleSheet("QLabel { color: " + warning_colour.name() + "; }");
853 ui->fallbackFeeWarningLabel->setIndent(GUIUtil::TextWidth(QFontMetrics(ui->fallbackFeeWarningLabel->font()), "x"));
854 }
855 else
856 {
857 ui->labelSmartFee2->hide();
858 ui->labelFeeEstimation->setText(tr("Estimated to begin confirmation within %n block(s).", "", returned_target));
859 ui->fallbackFeeWarningLabel->setVisible(false);
860 }
861
862 updateFeeMinimizedLabel();
863 }
864
865 // Coin Control: copy label "Quantity" to clipboard
866 void SendCoinsDialog::coinControlClipboardQuantity()
867 {
868 GUIUtil::setClipboard(ui->labelCoinControlQuantity->text());
869 }
870
871 // Coin Control: copy label "Amount" to clipboard
872 void SendCoinsDialog::coinControlClipboardAmount()
873 {
874 GUIUtil::setClipboard(ui->labelCoinControlAmount->text().left(ui->labelCoinControlAmount->text().indexOf(" ")));
875 }
876
877 // Coin Control: copy label "Fee" to clipboard
878 void SendCoinsDialog::coinControlClipboardFee()
879 {
880 GUIUtil::setClipboard(ui->labelCoinControlFee->text().left(ui->labelCoinControlFee->text().indexOf(" ")).replace(ASYMP_UTF8, ""));
881 }
882
883 // Coin Control: copy label "After fee" to clipboard
884 void SendCoinsDialog::coinControlClipboardAfterFee()
885 {
886 GUIUtil::setClipboard(ui->labelCoinControlAfterFee->text().left(ui->labelCoinControlAfterFee->text().indexOf(" ")).replace(ASYMP_UTF8, ""));
887 }
888
889 // Coin Control: copy label "Bytes" to clipboard
890 void SendCoinsDialog::coinControlClipboardBytes()
891 {
892 GUIUtil::setClipboard(ui->labelCoinControlBytes->text().replace(ASYMP_UTF8, ""));
893 }
894
895 // Coin Control: copy label "Change" to clipboard
896 void SendCoinsDialog::coinControlClipboardChange()
897 {
898 GUIUtil::setClipboard(ui->labelCoinControlChange->text().left(ui->labelCoinControlChange->text().indexOf(" ")).replace(ASYMP_UTF8, ""));
899 }
900
901 // Coin Control: settings menu - coin control enabled/disabled by user
902 void SendCoinsDialog::coinControlFeatureChanged(bool checked)
903 {
904 ui->frameCoinControl->setVisible(checked);
905
906 if (!checked && model) { // coin control features disabled
907 m_coin_control = std::make_unique<CCoinControl>();
908 }
909
910 coinControlUpdateLabels();
911 }
912
913 // Coin Control: button inputs -> show actual coin control dialog
914 void SendCoinsDialog::coinControlButtonClicked()
915 {
916 auto dlg = new CoinControlDialog(*m_coin_control, model, platformStyle);
917 connect(dlg, &QDialog::finished, this, &SendCoinsDialog::coinControlUpdateLabels);
918 GUIUtil::ShowModalDialogAsynchronously(dlg);
919 }
920
921 // Coin Control: checkbox custom change address
922 #if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0))
923 void SendCoinsDialog::coinControlChangeChecked(Qt::CheckState state)
924 #else
925 void SendCoinsDialog::coinControlChangeChecked(int state)
926 #endif
927 {
928 if (state == Qt::Unchecked)
929 {
930 m_coin_control->destChange = CNoDestination();
931 ui->labelCoinControlChangeLabel->clear();
932 }
933 else
934 // use this to re-validate an already entered address
935 coinControlChangeEdited(ui->lineEditCoinControlChange->text());
936
937 ui->lineEditCoinControlChange->setEnabled((state == Qt::Checked));
938 }
939
940 // Coin Control: custom change address changed
941 void SendCoinsDialog::coinControlChangeEdited(const QString& text)
942 {
943 if (model && model->getAddressTableModel())
944 {
945 // Default to no change address until verified
946 m_coin_control->destChange = CNoDestination();
947 ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:red;}");
948
949 const CTxDestination dest = DecodeDestination(text.toStdString());
950
951 if (text.isEmpty()) // Nothing entered
952 {
953 ui->labelCoinControlChangeLabel->setText("");
954 }
955 else if (!IsValidDestination(dest)) // Invalid address
956 {
957 ui->labelCoinControlChangeLabel->setText(tr("Warning: Invalid Bitcoin address"));
958 }
959 else // Valid address
960 {
961 if (!model->wallet().isSpendable(dest)) {
962 ui->labelCoinControlChangeLabel->setText(tr("Warning: Unknown change address"));
963
964 // confirmation dialog
965 QMessageBox::StandardButton btnRetVal = QMessageBox::question(this, tr("Confirm custom change address"), tr("The address you selected for change is not part of this wallet. Any or all funds in your wallet may be sent to this address. Are you sure?"),
966 QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel);
967
968 if(btnRetVal == QMessageBox::Yes)
969 m_coin_control->destChange = dest;
970 else
971 {
972 ui->lineEditCoinControlChange->setText("");
973 ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}");
974 ui->labelCoinControlChangeLabel->setText("");
975 }
976 }
977 else // Known change address
978 {
979 ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}");
980
981 // Query label
982 QString associatedLabel = model->getAddressTableModel()->labelForAddress(text);
983 if (!associatedLabel.isEmpty())
984 ui->labelCoinControlChangeLabel->setText(associatedLabel);
985 else
986 ui->labelCoinControlChangeLabel->setText(tr("(no label)"));
987
988 m_coin_control->destChange = dest;
989 }
990 }
991 }
992 }
993
994 // Coin Control: update labels
995 void SendCoinsDialog::coinControlUpdateLabels()
996 {
997 if (!model || !model->getOptionsModel())
998 return;
999
1000 updateCoinControlState();
1001
1002 // set pay amounts
1003 CoinControlDialog::payAmounts.clear();
1004 CoinControlDialog::fSubtractFeeFromAmount = false;
1005
1006 for(int i = 0; i < ui->entries->count(); ++i)
1007 {
1008 SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
1009 if(entry && !entry->isHidden())
1010 {
1011 SendCoinsRecipient rcp = entry->getValue();
1012 CoinControlDialog::payAmounts.append(rcp.amount);
1013 if (rcp.fSubtractFeeFromAmount)
1014 CoinControlDialog::fSubtractFeeFromAmount = true;
1015 }
1016 }
1017
1018 if (m_coin_control->HasSelected())
1019 {
1020 // actual coin control calculation
1021 CoinControlDialog::updateLabels(*m_coin_control, model, this);
1022
1023 // show coin control stats
1024 ui->labelCoinControlAutomaticallySelected->hide();
1025 ui->widgetCoinControl->show();
1026 }
1027 else
1028 {
1029 // hide coin control stats
1030 ui->labelCoinControlAutomaticallySelected->show();
1031 ui->widgetCoinControl->hide();
1032 ui->labelCoinControlInsuffFunds->hide();
1033 }
1034 }
1035
1036 SendConfirmationDialog::SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text, const QString& detailed_text, int _secDelay, bool enable_send, bool always_show_unsigned, QWidget* parent)
1037 : QMessageBox(parent), secDelay(_secDelay), m_enable_send(enable_send)
1038 {
1039 setIcon(QMessageBox::Question);
1040 setWindowTitle(title); // On macOS, the window title is ignored (as required by the macOS Guidelines).
1041 setText(text);
1042 setInformativeText(informative_text);
1043 setDetailedText(detailed_text);
1044 setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
1045 if (always_show_unsigned || !enable_send) addButton(QMessageBox::Save);
1046 setDefaultButton(QMessageBox::Cancel);
1047 yesButton = button(QMessageBox::Yes);
1048 if (confirmButtonText.isEmpty()) {
1049 confirmButtonText = yesButton->text();
1050 }
1051 m_psbt_button = button(QMessageBox::Save);
1052 updateButtons();
1053 connect(&countDownTimer, &QTimer::timeout, this, &SendConfirmationDialog::countDown);
1054 }
1055
1056 int SendConfirmationDialog::exec()
1057 {
1058 updateButtons();
1059 countDownTimer.start(1s);
1060 return QMessageBox::exec();
1061 }
1062
1063 void SendConfirmationDialog::countDown()
1064 {
1065 secDelay--;
1066 updateButtons();
1067
1068 if(secDelay <= 0)
1069 {
1070 countDownTimer.stop();
1071 }
1072 }
1073
1074 void SendConfirmationDialog::updateButtons()
1075 {
1076 if(secDelay > 0)
1077 {
1078 yesButton->setEnabled(false);
1079 yesButton->setText(confirmButtonText + (m_enable_send ? (" (" + QString::number(secDelay) + ")") : QString("")));
1080 if (m_psbt_button) {
1081 m_psbt_button->setEnabled(false);
1082 m_psbt_button->setText(m_psbt_button_text + " (" + QString::number(secDelay) + ")");
1083 }
1084 }
1085 else
1086 {
1087 yesButton->setEnabled(m_enable_send);
1088 yesButton->setText(confirmButtonText);
1089 if (m_psbt_button) {
1090 m_psbt_button->setEnabled(true);
1091 m_psbt_button->setText(m_psbt_button_text);
1092 }
1093 }
1094 }
1095