sendcoinsdialog.cpp raw

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