/ 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({.sign = true, .bip32_derivs = true}, /*n_signed=*/nullptr, psbtx, complete);
 451      } catch (const std::runtime_error& e) {
 452          QMessageBox::critical(nullptr, tr("Sign failed"), e.what());
 453          return false;
 454      }
 455      if (err == PSBTError::EXTERNAL_SIGNER_NOT_FOUND) {
 456          //: "External signer" means using devices such as hardware wallets.
 457          const QString msg = tr("External signer not found");
 458          QMessageBox::critical(nullptr, msg, msg);
 459          return false;
 460      }
 461      if (err == PSBTError::EXTERNAL_SIGNER_FAILED) {
 462          //: "External signer" means using devices such as hardware wallets.
 463          const QString msg = tr("External signer failure");
 464          QMessageBox::critical(nullptr, msg, msg);
 465          return false;
 466      }
 467      if (err) {
 468          qWarning() << "Failed to sign PSBT";
 469          processSendCoinsReturn(WalletModel::TransactionCreationFailed);
 470          return false;
 471      }
 472      // fillPSBT does not always properly finalize
 473      complete = FinalizeAndExtractPSBT(psbtx, mtx);
 474      return true;
 475  }
 476  
 477  void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
 478  {
 479      if(!model || !model->getOptionsModel())
 480          return;
 481  
 482      QString question_string, informative_text, detailed_text;
 483      if (!PrepareSendText(question_string, informative_text, detailed_text)) return;
 484      assert(m_current_transaction);
 485  
 486      const QString confirmation = tr("Confirm send coins");
 487      const bool enable_send{!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner()};
 488      const bool always_show_unsigned{model->getOptionsModel()->getEnablePSBTControls()};
 489      auto confirmationDialog = new SendConfirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, enable_send, always_show_unsigned, this);
 490      confirmationDialog->setAttribute(Qt::WA_DeleteOnClose);
 491      // TODO: Replace QDialog::exec() with safer QDialog::show().
 492      const auto retval = static_cast<QMessageBox::StandardButton>(confirmationDialog->exec());
 493  
 494      if(retval != QMessageBox::Yes && retval != QMessageBox::Save)
 495      {
 496          fNewRecipientAllowed = true;
 497          return;
 498      }
 499  
 500      bool send_failure = false;
 501      if (retval == QMessageBox::Save) {
 502          // "Create Unsigned" clicked
 503          CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
 504          PartiallySignedTransaction psbtx(mtx);
 505          bool complete = false;
 506          // Fill without signing
 507          const auto err{model->wallet().fillPSBT({.sign = false, .bip32_derivs = true}, /*n_signed=*/nullptr, psbtx, complete)};
 508          assert(!complete);
 509          assert(!err);
 510  
 511          // Copy PSBT to clipboard and offer to save
 512          presentPSBT(psbtx);
 513      } else {
 514          // "Send" clicked
 515          assert(!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner());
 516          bool broadcast = true;
 517          if (model->wallet().hasExternalSigner()) {
 518              CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
 519              PartiallySignedTransaction psbtx(mtx);
 520              bool complete = false;
 521              // Always fill without signing first. This prevents an external signer
 522              // from being called prematurely and is not expensive.
 523              const auto err{model->wallet().fillPSBT({.sign = false, .bip32_derivs = true}, /*n_signed=*/nullptr, psbtx, complete)};
 524              assert(!complete);
 525              assert(!err);
 526              send_failure = !signWithExternalSigner(psbtx, mtx, complete);
 527              // Don't broadcast when user rejects it on the device or there's a failure:
 528              broadcast = complete && !send_failure;
 529              if (!send_failure) {
 530                  // A transaction signed with an external signer is not always complete,
 531                  // e.g. in a multisig wallet.
 532                  if (complete) {
 533                      // Prepare transaction for broadcast transaction if complete
 534                      const CTransactionRef tx = MakeTransactionRef(mtx);
 535                      m_current_transaction->setWtx(tx);
 536                  } else {
 537                      presentPSBT(psbtx);
 538                  }
 539              }
 540          }
 541  
 542          // Broadcast the transaction, unless an external signer was used and it
 543          // failed, or more signatures are needed.
 544          if (broadcast) {
 545              // now send the prepared transaction
 546              model->sendCoins(*m_current_transaction);
 547              Q_EMIT coinsSent(m_current_transaction->getWtx()->GetHash());
 548          }
 549      }
 550      if (!send_failure) {
 551          accept();
 552          m_coin_control->UnSelectAll();
 553          coinControlUpdateLabels();
 554      }
 555      fNewRecipientAllowed = true;
 556      m_current_transaction.reset();
 557  }
 558  
 559  void SendCoinsDialog::clear()
 560  {
 561      m_current_transaction.reset();
 562  
 563      // Clear coin control settings
 564      m_coin_control->UnSelectAll();
 565      ui->checkBoxCoinControlChange->setChecked(false);
 566      ui->lineEditCoinControlChange->clear();
 567      coinControlUpdateLabels();
 568  
 569      // Remove entries until only one left
 570      while(ui->entries->count())
 571      {
 572          ui->entries->takeAt(0)->widget()->deleteLater();
 573      }
 574      addEntry();
 575  
 576      updateTabsAndLabels();
 577  }
 578  
 579  void SendCoinsDialog::reject()
 580  {
 581      clear();
 582  }
 583  
 584  void SendCoinsDialog::accept()
 585  {
 586      clear();
 587  }
 588  
 589  SendCoinsEntry *SendCoinsDialog::addEntry()
 590  {
 591      SendCoinsEntry *entry = new SendCoinsEntry(platformStyle, this);
 592      entry->setModel(model);
 593      ui->entries->addWidget(entry);
 594      connect(entry, &SendCoinsEntry::removeEntry, this, &SendCoinsDialog::removeEntry);
 595      connect(entry, &SendCoinsEntry::useAvailableBalance, this, &SendCoinsDialog::useAvailableBalance);
 596      connect(entry, &SendCoinsEntry::payAmountChanged, this, &SendCoinsDialog::coinControlUpdateLabels);
 597      connect(entry, &SendCoinsEntry::subtractFeeFromAmountChanged, this, &SendCoinsDialog::coinControlUpdateLabels);
 598  
 599      // Focus the field, so that entry can start immediately
 600      entry->clear();
 601      entry->setFocus();
 602      ui->scrollAreaWidgetContents->resize(ui->scrollAreaWidgetContents->sizeHint());
 603  
 604      // Scroll to the newly added entry on a QueuedConnection because Qt doesn't
 605      // adjust the scroll area and scrollbar immediately when the widget is added.
 606      // Invoking on a DirectConnection will only scroll to the second-to-last entry.
 607      QMetaObject::invokeMethod(ui->scrollArea, [this] {
 608          if (ui->scrollArea->verticalScrollBar()) {
 609              ui->scrollArea->verticalScrollBar()->setValue(ui->scrollArea->verticalScrollBar()->maximum());
 610          }
 611      }, Qt::QueuedConnection);
 612  
 613      updateTabsAndLabels();
 614      return entry;
 615  }
 616  
 617  void SendCoinsDialog::updateTabsAndLabels()
 618  {
 619      setupTabChain(nullptr);
 620      coinControlUpdateLabels();
 621  }
 622  
 623  void SendCoinsDialog::removeEntry(SendCoinsEntry* entry)
 624  {
 625      entry->hide();
 626  
 627      // If the last entry is about to be removed add an empty one
 628      if (ui->entries->count() == 1)
 629          addEntry();
 630  
 631      entry->deleteLater();
 632  
 633      updateTabsAndLabels();
 634  }
 635  
 636  QWidget *SendCoinsDialog::setupTabChain(QWidget *prev)
 637  {
 638      for(int i = 0; i < ui->entries->count(); ++i)
 639      {
 640          SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
 641          if(entry)
 642          {
 643              prev = entry->setupTabChain(prev);
 644          }
 645      }
 646      QWidget::setTabOrder(prev, ui->sendButton);
 647      QWidget::setTabOrder(ui->sendButton, ui->clearButton);
 648      QWidget::setTabOrder(ui->clearButton, ui->addButton);
 649      return ui->addButton;
 650  }
 651  
 652  void SendCoinsDialog::setAddress(const QString &address)
 653  {
 654      SendCoinsEntry *entry = nullptr;
 655      // Replace the first entry if it is still unused
 656      if(ui->entries->count() == 1)
 657      {
 658          SendCoinsEntry *first = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(0)->widget());
 659          if(first->isClear())
 660          {
 661              entry = first;
 662          }
 663      }
 664      if(!entry)
 665      {
 666          entry = addEntry();
 667      }
 668  
 669      entry->setAddress(address);
 670  }
 671  
 672  void SendCoinsDialog::pasteEntry(const SendCoinsRecipient &rv)
 673  {
 674      if(!fNewRecipientAllowed)
 675          return;
 676  
 677      SendCoinsEntry *entry = nullptr;
 678      // Replace the first entry if it is still unused
 679      if(ui->entries->count() == 1)
 680      {
 681          SendCoinsEntry *first = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(0)->widget());
 682          if(first->isClear())
 683          {
 684              entry = first;
 685          }
 686      }
 687      if(!entry)
 688      {
 689          entry = addEntry();
 690      }
 691  
 692      entry->setValue(rv);
 693      updateTabsAndLabels();
 694  }
 695  
 696  bool SendCoinsDialog::handlePaymentRequest(const SendCoinsRecipient &rv)
 697  {
 698      // Just paste the entry, all pre-checks
 699      // are done in paymentserver.cpp.
 700      pasteEntry(rv);
 701      return true;
 702  }
 703  
 704  void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances)
 705  {
 706      if(model && model->getOptionsModel())
 707      {
 708          CAmount balance = balances.balance;
 709          if (model->wallet().hasExternalSigner()) {
 710              ui->labelBalanceName->setText(tr("External balance:"));
 711          }
 712          ui->labelBalance->setText(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), balance));
 713      }
 714  }
 715  
 716  void SendCoinsDialog::refreshBalance()
 717  {
 718      setBalance(model->getCachedBalance());
 719      ui->customFee->setDisplayUnit(model->getOptionsModel()->getDisplayUnit());
 720      updateSmartFeeLabel();
 721  }
 722  
 723  void SendCoinsDialog::processSendCoinsReturn(const WalletModel::SendCoinsReturn &sendCoinsReturn, const QString &msgArg)
 724  {
 725      QPair<QString, CClientUIInterface::MessageBoxFlags> msgParams;
 726      // Default to a warning message, override if error message is needed
 727      msgParams.second = CClientUIInterface::MSG_WARNING;
 728  
 729      // This comment is specific to SendCoinsDialog usage of WalletModel::SendCoinsReturn.
 730      // All status values are used only in WalletModel::prepareTransaction()
 731      switch(sendCoinsReturn.status)
 732      {
 733      case WalletModel::InvalidAddress:
 734          msgParams.first = tr("The recipient address is not valid. Please recheck.");
 735          break;
 736      case WalletModel::InvalidAmount:
 737          msgParams.first = tr("The amount to pay must be larger than 0.");
 738          break;
 739      case WalletModel::AmountExceedsBalance:
 740          msgParams.first = tr("The amount exceeds your balance.");
 741          break;
 742      case WalletModel::DuplicateAddress:
 743          msgParams.first = tr("Duplicate address found: addresses should only be used once each.");
 744          break;
 745      case WalletModel::TransactionCreationFailed:
 746          msgParams.first = tr("Transaction creation failed!");
 747          msgParams.second = CClientUIInterface::MSG_ERROR;
 748          break;
 749      case WalletModel::AbsurdFee:
 750          msgParams.first = tr("A fee higher than %1 is considered an absurdly high fee.").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), model->wallet().getDefaultMaxTxFee()));
 751          break;
 752      case WalletModel::OK:
 753          return;
 754      } // no default case, so the compiler can warn about missing cases
 755      Q_EMIT message(tr("Send Coins"), msgParams.first, msgParams.second);
 756  }
 757  
 758  void SendCoinsDialog::minimizeFeeSection(bool fMinimize)
 759  {
 760      ui->labelFeeMinimized->setVisible(fMinimize);
 761      ui->buttonChooseFee  ->setVisible(fMinimize);
 762      ui->buttonMinimizeFee->setVisible(!fMinimize);
 763      ui->frameFeeSelection->setVisible(!fMinimize);
 764      ui->horizontalLayoutSmartFee->setContentsMargins(0, (fMinimize ? 0 : 6), 0, 0);
 765      fFeeMinimized = fMinimize;
 766  }
 767  
 768  void SendCoinsDialog::on_buttonChooseFee_clicked()
 769  {
 770      minimizeFeeSection(false);
 771  }
 772  
 773  void SendCoinsDialog::on_buttonMinimizeFee_clicked()
 774  {
 775      updateFeeMinimizedLabel();
 776      minimizeFeeSection(true);
 777  }
 778  
 779  void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry)
 780  {
 781      // Same behavior as send: if we have selected coins, only obtain their available balance.
 782      // Copy to avoid modifying the member's data.
 783      CCoinControl coin_control = *m_coin_control;
 784      coin_control.m_allow_other_inputs = !coin_control.HasSelected();
 785  
 786      // Calculate available amount to send.
 787      CAmount amount = model->getAvailableBalance(&coin_control);
 788      for (int i = 0; i < ui->entries->count(); ++i) {
 789          SendCoinsEntry* e = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
 790          if (e && !e->isHidden() && e != entry) {
 791              amount -= e->getValue().amount;
 792          }
 793      }
 794  
 795      if (amount > 0) {
 796        entry->checkSubtractFeeFromAmount();
 797        entry->setAmount(amount);
 798      } else {
 799        entry->setAmount(0);
 800      }
 801  }
 802  
 803  void SendCoinsDialog::updateFeeSectionControls()
 804  {
 805      ui->confTargetSelector      ->setEnabled(ui->radioSmartFee->isChecked());
 806      ui->labelSmartFee           ->setEnabled(ui->radioSmartFee->isChecked());
 807      ui->labelSmartFee2          ->setEnabled(ui->radioSmartFee->isChecked());
 808      ui->labelSmartFee3          ->setEnabled(ui->radioSmartFee->isChecked());
 809      ui->labelFeeEstimation      ->setEnabled(ui->radioSmartFee->isChecked());
 810      ui->labelCustomFeeWarning   ->setEnabled(ui->radioCustomFee->isChecked());
 811      ui->labelCustomPerKilobyte  ->setEnabled(ui->radioCustomFee->isChecked());
 812      ui->customFee               ->setEnabled(ui->radioCustomFee->isChecked());
 813  }
 814  
 815  void SendCoinsDialog::updateFeeMinimizedLabel()
 816  {
 817      if(!model || !model->getOptionsModel())
 818          return;
 819  
 820      if (ui->radioSmartFee->isChecked())
 821          ui->labelFeeMinimized->setText(ui->labelSmartFee->text());
 822      else {
 823          ui->labelFeeMinimized->setText(tr("%1/kvB").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), ui->customFee->value())));
 824      }
 825  }
 826  
 827  void SendCoinsDialog::updateCoinControlState()
 828  {
 829      if (ui->radioCustomFee->isChecked()) {
 830          m_coin_control->m_feerate = CFeeRate(ui->customFee->value());
 831      } else {
 832          m_coin_control->m_feerate.reset();
 833      }
 834      // Avoid using global defaults when sending money from the GUI
 835      // Either custom fee will be used or if not selected, the confirmation target from dropdown box
 836      m_coin_control->m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex());
 837      m_coin_control->m_signal_bip125_rbf = ui->optInRBF->isChecked();
 838  }
 839  
 840  void SendCoinsDialog::updateNumberOfBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, SyncType synctype, SynchronizationState sync_state) {
 841      // During shutdown, clientModel will be nullptr. Attempting to update views at this point may cause a crash
 842      // due to accessing backend models that might no longer exist.
 843      if (!clientModel) return;
 844      // Process event
 845      if (sync_state == SynchronizationState::POST_INIT) {
 846          updateSmartFeeLabel();
 847      }
 848  }
 849  
 850  void SendCoinsDialog::updateSmartFeeLabel()
 851  {
 852      if(!model || !model->getOptionsModel())
 853          return;
 854      updateCoinControlState();
 855      m_coin_control->m_feerate.reset(); // Explicitly use only fee estimation rate for smart fee labels
 856      int returned_target;
 857      FeeReason reason;
 858      CFeeRate feeRate = CFeeRate(model->wallet().getMinimumFee(1000, *m_coin_control, &returned_target, &reason));
 859  
 860      ui->labelSmartFee->setText(tr("%1/kvB").arg(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), feeRate.GetFeePerK())));
 861  
 862      if (reason == FeeReason::FALLBACK) {
 863          ui->labelSmartFee2->show(); // (Smart fee not initialized yet. This usually takes a few blocks...)
 864          ui->labelFeeEstimation->setText("");
 865          ui->fallbackFeeWarningLabel->setVisible(true);
 866          int lightness = ui->fallbackFeeWarningLabel->palette().color(QPalette::WindowText).lightness();
 867          QColor warning_colour(255 - (lightness / 5), 176 - (lightness / 3), 48 - (lightness / 14));
 868          ui->fallbackFeeWarningLabel->setStyleSheet("QLabel { color: " + warning_colour.name() + "; }");
 869          ui->fallbackFeeWarningLabel->setIndent(GUIUtil::TextWidth(QFontMetrics(ui->fallbackFeeWarningLabel->font()), "x"));
 870      }
 871      else
 872      {
 873          ui->labelSmartFee2->hide();
 874          ui->labelFeeEstimation->setText(tr("Estimated to begin confirmation within %n block(s).", "", returned_target));
 875          ui->fallbackFeeWarningLabel->setVisible(false);
 876      }
 877  
 878      updateFeeMinimizedLabel();
 879  }
 880  
 881  // Coin Control: copy label "Quantity" to clipboard
 882  void SendCoinsDialog::coinControlClipboardQuantity()
 883  {
 884      GUIUtil::setClipboard(ui->labelCoinControlQuantity->text());
 885  }
 886  
 887  // Coin Control: copy label "Amount" to clipboard
 888  void SendCoinsDialog::coinControlClipboardAmount()
 889  {
 890      GUIUtil::setClipboard(ui->labelCoinControlAmount->text().left(ui->labelCoinControlAmount->text().indexOf(" ")));
 891  }
 892  
 893  // Coin Control: copy label "Fee" to clipboard
 894  void SendCoinsDialog::coinControlClipboardFee()
 895  {
 896      GUIUtil::setClipboard(ui->labelCoinControlFee->text().left(ui->labelCoinControlFee->text().indexOf(" ")).replace(ASYMP_UTF8, ""));
 897  }
 898  
 899  // Coin Control: copy label "After fee" to clipboard
 900  void SendCoinsDialog::coinControlClipboardAfterFee()
 901  {
 902      GUIUtil::setClipboard(ui->labelCoinControlAfterFee->text().left(ui->labelCoinControlAfterFee->text().indexOf(" ")).replace(ASYMP_UTF8, ""));
 903  }
 904  
 905  // Coin Control: copy label "Bytes" to clipboard
 906  void SendCoinsDialog::coinControlClipboardBytes()
 907  {
 908      GUIUtil::setClipboard(ui->labelCoinControlBytes->text().replace(ASYMP_UTF8, ""));
 909  }
 910  
 911  // Coin Control: copy label "Change" to clipboard
 912  void SendCoinsDialog::coinControlClipboardChange()
 913  {
 914      GUIUtil::setClipboard(ui->labelCoinControlChange->text().left(ui->labelCoinControlChange->text().indexOf(" ")).replace(ASYMP_UTF8, ""));
 915  }
 916  
 917  // Coin Control: settings menu - coin control enabled/disabled by user
 918  void SendCoinsDialog::coinControlFeatureChanged(bool checked)
 919  {
 920      ui->frameCoinControl->setVisible(checked);
 921  
 922      if (!checked && model) { // coin control features disabled
 923          m_coin_control = std::make_unique<CCoinControl>();
 924      }
 925  
 926      coinControlUpdateLabels();
 927  }
 928  
 929  // Coin Control: button inputs -> show actual coin control dialog
 930  void SendCoinsDialog::coinControlButtonClicked()
 931  {
 932      auto dlg = new CoinControlDialog(*m_coin_control, model, platformStyle);
 933      connect(dlg, &QDialog::finished, this, &SendCoinsDialog::coinControlUpdateLabels);
 934      GUIUtil::ShowModalDialogAsynchronously(dlg);
 935  }
 936  
 937  // Coin Control: checkbox custom change address
 938  #if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0))
 939  void SendCoinsDialog::coinControlChangeChecked(Qt::CheckState state)
 940  #else
 941  void SendCoinsDialog::coinControlChangeChecked(int state)
 942  #endif
 943  {
 944      if (state == Qt::Unchecked)
 945      {
 946          m_coin_control->destChange = CNoDestination();
 947          ui->labelCoinControlChangeLabel->clear();
 948      }
 949      else
 950          // use this to re-validate an already entered address
 951          coinControlChangeEdited(ui->lineEditCoinControlChange->text());
 952  
 953      ui->lineEditCoinControlChange->setEnabled((state == Qt::Checked));
 954  }
 955  
 956  // Coin Control: custom change address changed
 957  void SendCoinsDialog::coinControlChangeEdited(const QString& text)
 958  {
 959      if (model && model->getAddressTableModel())
 960      {
 961          // Default to no change address until verified
 962          m_coin_control->destChange = CNoDestination();
 963          ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:red;}");
 964  
 965          const CTxDestination dest = DecodeDestination(text.toStdString());
 966  
 967          if (text.isEmpty()) // Nothing entered
 968          {
 969              ui->labelCoinControlChangeLabel->setText("");
 970          }
 971          else if (!IsValidDestination(dest)) // Invalid address
 972          {
 973              ui->labelCoinControlChangeLabel->setText(tr("Warning: Invalid Bitcoin address"));
 974          }
 975          else // Valid address
 976          {
 977              if (!model->wallet().isSpendable(dest)) {
 978                  ui->labelCoinControlChangeLabel->setText(tr("Warning: Unknown change address"));
 979  
 980                  // confirmation dialog
 981                  QMessageBox::StandardButton btnRetVal = QMessageBox::question(this, tr("Confirm custom change address"), tr("The address you selected for change is not part of this wallet. Any or all funds in your wallet may be sent to this address. Are you sure?"),
 982                      QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel);
 983  
 984                  if(btnRetVal == QMessageBox::Yes)
 985                      m_coin_control->destChange = dest;
 986                  else
 987                  {
 988                      ui->lineEditCoinControlChange->setText("");
 989                      ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}");
 990                      ui->labelCoinControlChangeLabel->setText("");
 991                  }
 992              }
 993              else // Known change address
 994              {
 995                  ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}");
 996  
 997                  // Query label
 998                  QString associatedLabel = model->getAddressTableModel()->labelForAddress(text);
 999                  if (!associatedLabel.isEmpty())
1000                      ui->labelCoinControlChangeLabel->setText(associatedLabel);
1001                  else
1002                      ui->labelCoinControlChangeLabel->setText(tr("(no label)"));
1003  
1004                  m_coin_control->destChange = dest;
1005              }
1006          }
1007      }
1008  }
1009  
1010  // Coin Control: update labels
1011  void SendCoinsDialog::coinControlUpdateLabels()
1012  {
1013      if (!model || !model->getOptionsModel())
1014          return;
1015  
1016      updateCoinControlState();
1017  
1018      // set pay amounts
1019      CoinControlDialog::payAmounts.clear();
1020      CoinControlDialog::fSubtractFeeFromAmount = false;
1021  
1022      for(int i = 0; i < ui->entries->count(); ++i)
1023      {
1024          SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
1025          if(entry && !entry->isHidden())
1026          {
1027              SendCoinsRecipient rcp = entry->getValue();
1028              CoinControlDialog::payAmounts.append(rcp.amount);
1029              if (rcp.fSubtractFeeFromAmount)
1030                  CoinControlDialog::fSubtractFeeFromAmount = true;
1031          }
1032      }
1033  
1034      if (m_coin_control->HasSelected())
1035      {
1036          // actual coin control calculation
1037          CoinControlDialog::updateLabels(*m_coin_control, model, this);
1038  
1039          // show coin control stats
1040          ui->labelCoinControlAutomaticallySelected->hide();
1041          ui->widgetCoinControl->show();
1042      }
1043      else
1044      {
1045          // hide coin control stats
1046          ui->labelCoinControlAutomaticallySelected->show();
1047          ui->widgetCoinControl->hide();
1048          ui->labelCoinControlInsuffFunds->hide();
1049      }
1050  }
1051  
1052  SendConfirmationDialog::SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text, const QString& detailed_text, int _secDelay, bool enable_send, bool always_show_unsigned, QWidget* parent)
1053      : QMessageBox(parent), secDelay(_secDelay), m_enable_send(enable_send)
1054  {
1055      setIcon(QMessageBox::Question);
1056      setWindowTitle(title); // On macOS, the window title is ignored (as required by the macOS Guidelines).
1057      setText(text);
1058      setInformativeText(informative_text);
1059      setDetailedText(detailed_text);
1060      setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
1061      if (always_show_unsigned || !enable_send) addButton(QMessageBox::Save);
1062      setDefaultButton(QMessageBox::Cancel);
1063      yesButton = button(QMessageBox::Yes);
1064      if (confirmButtonText.isEmpty()) {
1065          confirmButtonText = yesButton->text();
1066      }
1067      m_psbt_button = button(QMessageBox::Save);
1068      updateButtons();
1069      connect(&countDownTimer, &QTimer::timeout, this, &SendConfirmationDialog::countDown);
1070  }
1071  
1072  int SendConfirmationDialog::exec()
1073  {
1074      updateButtons();
1075      countDownTimer.start(1s);
1076      return QMessageBox::exec();
1077  }
1078  
1079  void SendConfirmationDialog::countDown()
1080  {
1081      secDelay--;
1082      updateButtons();
1083  
1084      if(secDelay <= 0)
1085      {
1086          countDownTimer.stop();
1087      }
1088  }
1089  
1090  void SendConfirmationDialog::updateButtons()
1091  {
1092      if(secDelay > 0)
1093      {
1094          yesButton->setEnabled(false);
1095          yesButton->setText(confirmButtonText + (m_enable_send ? (" (" + QString::number(secDelay) + ")") : QString("")));
1096          if (m_psbt_button) {
1097              m_psbt_button->setEnabled(false);
1098              m_psbt_button->setText(m_psbt_button_text + " (" + QString::number(secDelay) + ")");
1099          }
1100      }
1101      else
1102      {
1103          yesButton->setEnabled(m_enable_send);
1104          yesButton->setText(confirmButtonText);
1105          if (m_psbt_button) {
1106              m_psbt_button->setEnabled(true);
1107              m_psbt_button->setText(m_psbt_button_text);
1108          }
1109      }
1110  }