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(std::nullopt, /*sign=*/true, /*bip32derivs=*/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(std::nullopt, /*sign=*/false, /*bip32derivs=*/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(std::nullopt, /*sign=*/false, /*bip32derivs=*/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 // included to prevent a compiler warning. 753 case WalletModel::OK: 754 default: 755 return; 756 } 757 758 Q_EMIT message(tr("Send Coins"), msgParams.first, msgParams.second); 759 } 760 761 void SendCoinsDialog::minimizeFeeSection(bool fMinimize) 762 { 763 ui->labelFeeMinimized->setVisible(fMinimize); 764 ui->buttonChooseFee ->setVisible(fMinimize); 765 ui->buttonMinimizeFee->setVisible(!fMinimize); 766 ui->frameFeeSelection->setVisible(!fMinimize); 767 ui->horizontalLayoutSmartFee->setContentsMargins(0, (fMinimize ? 0 : 6), 0, 0); 768 fFeeMinimized = fMinimize; 769 } 770 771 void SendCoinsDialog::on_buttonChooseFee_clicked() 772 { 773 minimizeFeeSection(false); 774 } 775 776 void SendCoinsDialog::on_buttonMinimizeFee_clicked() 777 { 778 updateFeeMinimizedLabel(); 779 minimizeFeeSection(true); 780 } 781 782 void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry) 783 { 784 // Same behavior as send: if we have selected coins, only obtain their available balance. 785 // Copy to avoid modifying the member's data. 786 CCoinControl coin_control = *m_coin_control; 787 coin_control.m_allow_other_inputs = !coin_control.HasSelected(); 788 789 // Calculate available amount to send. 790 CAmount amount = model->getAvailableBalance(&coin_control); 791 for (int i = 0; i < ui->entries->count(); ++i) { 792 SendCoinsEntry* e = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget()); 793 if (e && !e->isHidden() && e != entry) { 794 amount -= e->getValue().amount; 795 } 796 } 797 798 if (amount > 0) { 799 entry->checkSubtractFeeFromAmount(); 800 entry->setAmount(amount); 801 } else { 802 entry->setAmount(0); 803 } 804 } 805 806 void SendCoinsDialog::updateFeeSectionControls() 807 { 808 ui->confTargetSelector ->setEnabled(ui->radioSmartFee->isChecked()); 809 ui->labelSmartFee ->setEnabled(ui->radioSmartFee->isChecked()); 810 ui->labelSmartFee2 ->setEnabled(ui->radioSmartFee->isChecked()); 811 ui->labelSmartFee3 ->setEnabled(ui->radioSmartFee->isChecked()); 812 ui->labelFeeEstimation ->setEnabled(ui->radioSmartFee->isChecked()); 813 ui->labelCustomFeeWarning ->setEnabled(ui->radioCustomFee->isChecked()); 814 ui->labelCustomPerKilobyte ->setEnabled(ui->radioCustomFee->isChecked()); 815 ui->customFee ->setEnabled(ui->radioCustomFee->isChecked()); 816 } 817 818 void SendCoinsDialog::updateFeeMinimizedLabel() 819 { 820 if(!model || !model->getOptionsModel()) 821 return; 822 823 if (ui->radioSmartFee->isChecked()) 824 ui->labelFeeMinimized->setText(ui->labelSmartFee->text()); 825 else { 826 ui->labelFeeMinimized->setText(tr("%1/kvB").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), ui->customFee->value()))); 827 } 828 } 829 830 void SendCoinsDialog::updateCoinControlState() 831 { 832 if (ui->radioCustomFee->isChecked()) { 833 m_coin_control->m_feerate = CFeeRate(ui->customFee->value()); 834 } else { 835 m_coin_control->m_feerate.reset(); 836 } 837 // Avoid using global defaults when sending money from the GUI 838 // Either custom fee will be used or if not selected, the confirmation target from dropdown box 839 m_coin_control->m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex()); 840 m_coin_control->m_signal_bip125_rbf = ui->optInRBF->isChecked(); 841 } 842 843 void SendCoinsDialog::updateNumberOfBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state) { 844 // During shutdown, clientModel will be nullptr. Attempting to update views at this point may cause a crash 845 // due to accessing backend models that might no longer exist. 846 if (!clientModel) return; 847 // Process event 848 if (sync_state == SynchronizationState::POST_INIT) { 849 updateSmartFeeLabel(); 850 } 851 } 852 853 void SendCoinsDialog::updateSmartFeeLabel() 854 { 855 if(!model || !model->getOptionsModel()) 856 return; 857 updateCoinControlState(); 858 m_coin_control->m_feerate.reset(); // Explicitly use only fee estimation rate for smart fee labels 859 int returned_target; 860 FeeReason reason; 861 CFeeRate feeRate = CFeeRate(model->wallet().getMinimumFee(1000, *m_coin_control, &returned_target, &reason)); 862 863 ui->labelSmartFee->setText(tr("%1/kvB").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), feeRate.GetFeePerK()))); 864 865 if (reason == FeeReason::FALLBACK) { 866 ui->labelSmartFee2->show(); // (Smart fee not initialized yet. This usually takes a few blocks...) 867 ui->labelFeeEstimation->setText(""); 868 ui->fallbackFeeWarningLabel->setVisible(true); 869 int lightness = ui->fallbackFeeWarningLabel->palette().color(QPalette::WindowText).lightness(); 870 QColor warning_colour(255 - (lightness / 5), 176 - (lightness / 3), 48 - (lightness / 14)); 871 ui->fallbackFeeWarningLabel->setStyleSheet("QLabel { color: " + warning_colour.name() + "; }"); 872 ui->fallbackFeeWarningLabel->setIndent(GUIUtil::TextWidth(QFontMetrics(ui->fallbackFeeWarningLabel->font()), "x")); 873 } 874 else 875 { 876 ui->labelSmartFee2->hide(); 877 ui->labelFeeEstimation->setText(tr("Estimated to begin confirmation within %n block(s).", "", returned_target)); 878 ui->fallbackFeeWarningLabel->setVisible(false); 879 } 880 881 updateFeeMinimizedLabel(); 882 } 883 884 // Coin Control: copy label "Quantity" to clipboard 885 void SendCoinsDialog::coinControlClipboardQuantity() 886 { 887 GUIUtil::setClipboard(ui->labelCoinControlQuantity->text()); 888 } 889 890 // Coin Control: copy label "Amount" to clipboard 891 void SendCoinsDialog::coinControlClipboardAmount() 892 { 893 GUIUtil::setClipboard(ui->labelCoinControlAmount->text().left(ui->labelCoinControlAmount->text().indexOf(" "))); 894 } 895 896 // Coin Control: copy label "Fee" to clipboard 897 void SendCoinsDialog::coinControlClipboardFee() 898 { 899 GUIUtil::setClipboard(ui->labelCoinControlFee->text().left(ui->labelCoinControlFee->text().indexOf(" ")).replace(ASYMP_UTF8, "")); 900 } 901 902 // Coin Control: copy label "After fee" to clipboard 903 void SendCoinsDialog::coinControlClipboardAfterFee() 904 { 905 GUIUtil::setClipboard(ui->labelCoinControlAfterFee->text().left(ui->labelCoinControlAfterFee->text().indexOf(" ")).replace(ASYMP_UTF8, "")); 906 } 907 908 // Coin Control: copy label "Bytes" to clipboard 909 void SendCoinsDialog::coinControlClipboardBytes() 910 { 911 GUIUtil::setClipboard(ui->labelCoinControlBytes->text().replace(ASYMP_UTF8, "")); 912 } 913 914 // Coin Control: copy label "Change" to clipboard 915 void SendCoinsDialog::coinControlClipboardChange() 916 { 917 GUIUtil::setClipboard(ui->labelCoinControlChange->text().left(ui->labelCoinControlChange->text().indexOf(" ")).replace(ASYMP_UTF8, "")); 918 } 919 920 // Coin Control: settings menu - coin control enabled/disabled by user 921 void SendCoinsDialog::coinControlFeatureChanged(bool checked) 922 { 923 ui->frameCoinControl->setVisible(checked); 924 925 if (!checked && model) { // coin control features disabled 926 m_coin_control = std::make_unique<CCoinControl>(); 927 } 928 929 coinControlUpdateLabels(); 930 } 931 932 // Coin Control: button inputs -> show actual coin control dialog 933 void SendCoinsDialog::coinControlButtonClicked() 934 { 935 auto dlg = new CoinControlDialog(*m_coin_control, model, platformStyle); 936 connect(dlg, &QDialog::finished, this, &SendCoinsDialog::coinControlUpdateLabels); 937 GUIUtil::ShowModalDialogAsynchronously(dlg); 938 } 939 940 // Coin Control: checkbox custom change address 941 #if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)) 942 void SendCoinsDialog::coinControlChangeChecked(Qt::CheckState state) 943 #else 944 void SendCoinsDialog::coinControlChangeChecked(int state) 945 #endif 946 { 947 if (state == Qt::Unchecked) 948 { 949 m_coin_control->destChange = CNoDestination(); 950 ui->labelCoinControlChangeLabel->clear(); 951 } 952 else 953 // use this to re-validate an already entered address 954 coinControlChangeEdited(ui->lineEditCoinControlChange->text()); 955 956 ui->lineEditCoinControlChange->setEnabled((state == Qt::Checked)); 957 } 958 959 // Coin Control: custom change address changed 960 void SendCoinsDialog::coinControlChangeEdited(const QString& text) 961 { 962 if (model && model->getAddressTableModel()) 963 { 964 // Default to no change address until verified 965 m_coin_control->destChange = CNoDestination(); 966 ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:red;}"); 967 968 const CTxDestination dest = DecodeDestination(text.toStdString()); 969 970 if (text.isEmpty()) // Nothing entered 971 { 972 ui->labelCoinControlChangeLabel->setText(""); 973 } 974 else if (!IsValidDestination(dest)) // Invalid address 975 { 976 ui->labelCoinControlChangeLabel->setText(tr("Warning: Invalid Bitcoin address")); 977 } 978 else // Valid address 979 { 980 if (!model->wallet().isSpendable(dest)) { 981 ui->labelCoinControlChangeLabel->setText(tr("Warning: Unknown change address")); 982 983 // confirmation dialog 984 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?"), 985 QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel); 986 987 if(btnRetVal == QMessageBox::Yes) 988 m_coin_control->destChange = dest; 989 else 990 { 991 ui->lineEditCoinControlChange->setText(""); 992 ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}"); 993 ui->labelCoinControlChangeLabel->setText(""); 994 } 995 } 996 else // Known change address 997 { 998 ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}"); 999 1000 // Query label 1001 QString associatedLabel = model->getAddressTableModel()->labelForAddress(text); 1002 if (!associatedLabel.isEmpty()) 1003 ui->labelCoinControlChangeLabel->setText(associatedLabel); 1004 else 1005 ui->labelCoinControlChangeLabel->setText(tr("(no label)")); 1006 1007 m_coin_control->destChange = dest; 1008 } 1009 } 1010 } 1011 } 1012 1013 // Coin Control: update labels 1014 void SendCoinsDialog::coinControlUpdateLabels() 1015 { 1016 if (!model || !model->getOptionsModel()) 1017 return; 1018 1019 updateCoinControlState(); 1020 1021 // set pay amounts 1022 CoinControlDialog::payAmounts.clear(); 1023 CoinControlDialog::fSubtractFeeFromAmount = false; 1024 1025 for(int i = 0; i < ui->entries->count(); ++i) 1026 { 1027 SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget()); 1028 if(entry && !entry->isHidden()) 1029 { 1030 SendCoinsRecipient rcp = entry->getValue(); 1031 CoinControlDialog::payAmounts.append(rcp.amount); 1032 if (rcp.fSubtractFeeFromAmount) 1033 CoinControlDialog::fSubtractFeeFromAmount = true; 1034 } 1035 } 1036 1037 if (m_coin_control->HasSelected()) 1038 { 1039 // actual coin control calculation 1040 CoinControlDialog::updateLabels(*m_coin_control, model, this); 1041 1042 // show coin control stats 1043 ui->labelCoinControlAutomaticallySelected->hide(); 1044 ui->widgetCoinControl->show(); 1045 } 1046 else 1047 { 1048 // hide coin control stats 1049 ui->labelCoinControlAutomaticallySelected->show(); 1050 ui->widgetCoinControl->hide(); 1051 ui->labelCoinControlInsuffFunds->hide(); 1052 } 1053 } 1054 1055 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) 1056 : QMessageBox(parent), secDelay(_secDelay), m_enable_send(enable_send) 1057 { 1058 setIcon(QMessageBox::Question); 1059 setWindowTitle(title); // On macOS, the window title is ignored (as required by the macOS Guidelines). 1060 setText(text); 1061 setInformativeText(informative_text); 1062 setDetailedText(detailed_text); 1063 setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); 1064 if (always_show_unsigned || !enable_send) addButton(QMessageBox::Save); 1065 setDefaultButton(QMessageBox::Cancel); 1066 yesButton = button(QMessageBox::Yes); 1067 if (confirmButtonText.isEmpty()) { 1068 confirmButtonText = yesButton->text(); 1069 } 1070 m_psbt_button = button(QMessageBox::Save); 1071 updateButtons(); 1072 connect(&countDownTimer, &QTimer::timeout, this, &SendConfirmationDialog::countDown); 1073 } 1074 1075 int SendConfirmationDialog::exec() 1076 { 1077 updateButtons(); 1078 countDownTimer.start(1s); 1079 return QMessageBox::exec(); 1080 } 1081 1082 void SendConfirmationDialog::countDown() 1083 { 1084 secDelay--; 1085 updateButtons(); 1086 1087 if(secDelay <= 0) 1088 { 1089 countDownTimer.stop(); 1090 } 1091 } 1092 1093 void SendConfirmationDialog::updateButtons() 1094 { 1095 if(secDelay > 0) 1096 { 1097 yesButton->setEnabled(false); 1098 yesButton->setText(confirmButtonText + (m_enable_send ? (" (" + QString::number(secDelay) + ")") : QString(""))); 1099 if (m_psbt_button) { 1100 m_psbt_button->setEnabled(false); 1101 m_psbt_button->setText(m_psbt_button_text + " (" + QString::number(secDelay) + ")"); 1102 } 1103 } 1104 else 1105 { 1106 yesButton->setEnabled(m_enable_send); 1107 yesButton->setText(confirmButtonText); 1108 if (m_psbt_button) { 1109 m_psbt_button->setEnabled(true); 1110 m_psbt_button->setText(m_psbt_button_text); 1111 } 1112 } 1113 }