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