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