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