/ src / qt / sendcoinsdialog.cpp
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  }