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