sendcoinsdialog.cpp raw

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