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