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