/ src / qt / walletmodel.cpp
walletmodel.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 <qt/walletmodel.h>
  6  
  7  #include <qt/addresstablemodel.h>
  8  #include <qt/clientmodel.h>
  9  #include <qt/guiconstants.h>
 10  #include <qt/guiutil.h>
 11  #include <qt/optionsmodel.h>
 12  #include <qt/paymentserver.h>
 13  #include <qt/recentrequeststablemodel.h>
 14  #include <qt/sendcoinsdialog.h>
 15  #include <qt/transactiontablemodel.h>
 16  
 17  #include <common/args.h>
 18  #include <interfaces/handler.h>
 19  #include <interfaces/node.h>
 20  #include <key_io.h>
 21  #include <node/interface_ui.h>
 22  #include <node/types.h>
 23  #include <psbt.h>
 24  #include <util/translation.h>
 25  #include <wallet/coincontrol.h>
 26  #include <wallet/types.h>
 27  #include <wallet/wallet.h>
 28  
 29  #include <cstdint>
 30  #include <functional>
 31  #include <memory>
 32  #include <vector>
 33  
 34  #include <QDebug>
 35  #include <QMessageBox>
 36  #include <QSet>
 37  #include <QTimer>
 38  
 39  using wallet::CCoinControl;
 40  using wallet::CRecipient;
 41  using wallet::DEFAULT_DISABLE_WALLET;
 42  
 43  WalletModel::WalletModel(std::unique_ptr<interfaces::Wallet> wallet, ClientModel& client_model, const PlatformStyle *platformStyle, QObject *parent) :
 44      QObject(parent),
 45      m_wallet(std::move(wallet)),
 46      m_client_model(&client_model),
 47      m_node(client_model.node()),
 48      optionsModel(client_model.getOptionsModel()),
 49      timer(new QTimer(this))
 50  {
 51      addressTableModel = new AddressTableModel(this);
 52      transactionTableModel = new TransactionTableModel(platformStyle, this);
 53      recentRequestsTableModel = new RecentRequestsTableModel(this);
 54  
 55      subscribeToCoreSignals();
 56  }
 57  
 58  WalletModel::~WalletModel()
 59  {
 60      unsubscribeFromCoreSignals();
 61  }
 62  
 63  void WalletModel::startPollBalance()
 64  {
 65      // Update the cached balance right away, so every view can make use of it,
 66      // so them don't need to waste resources recalculating it.
 67      pollBalanceChanged();
 68  
 69      // This timer will be fired repeatedly to update the balance
 70      // Since the QTimer::timeout is a private signal, it cannot be used
 71      // in the GUIUtil::ExceptionSafeConnect directly.
 72      connect(timer, &QTimer::timeout, this, &WalletModel::timerTimeout);
 73      GUIUtil::ExceptionSafeConnect(this, &WalletModel::timerTimeout, this, &WalletModel::pollBalanceChanged);
 74      timer->start(MODEL_UPDATE_DELAY);
 75  }
 76  
 77  void WalletModel::setClientModel(ClientModel* client_model)
 78  {
 79      m_client_model = client_model;
 80      if (!m_client_model) timer->stop();
 81  }
 82  
 83  void WalletModel::updateStatus()
 84  {
 85      EncryptionStatus newEncryptionStatus = getEncryptionStatus();
 86  
 87      if(cachedEncryptionStatus != newEncryptionStatus) {
 88          Q_EMIT encryptionStatusChanged();
 89      }
 90  }
 91  
 92  void WalletModel::pollBalanceChanged()
 93  {
 94      // Avoid recomputing wallet balances unless a TransactionChanged or
 95      // BlockTip notification was received.
 96      if (!fForceCheckBalanceChanged && m_cached_last_update_tip == getLastBlockProcessed()) return;
 97  
 98      // Try to get balances and return early if locks can't be acquired. This
 99      // avoids the GUI from getting stuck on periodical polls if the core is
100      // holding the locks for a longer time - for example, during a wallet
101      // rescan.
102      interfaces::WalletBalances new_balances;
103      uint256 block_hash;
104      if (!m_wallet->tryGetBalances(new_balances, block_hash)) {
105          return;
106      }
107  
108      if (fForceCheckBalanceChanged || block_hash != m_cached_last_update_tip) {
109          fForceCheckBalanceChanged = false;
110  
111          // Balance and number of transactions might have changed
112          m_cached_last_update_tip = block_hash;
113  
114          checkBalanceChanged(new_balances);
115          if(transactionTableModel)
116              transactionTableModel->updateConfirmations();
117      }
118  }
119  
120  void WalletModel::checkBalanceChanged(const interfaces::WalletBalances& new_balances)
121  {
122      if (new_balances.balanceChanged(m_cached_balances)) {
123          m_cached_balances = new_balances;
124          Q_EMIT balanceChanged(new_balances);
125      }
126  }
127  
128  interfaces::WalletBalances WalletModel::getCachedBalance() const
129  {
130      return m_cached_balances;
131  }
132  
133  void WalletModel::updateTransaction()
134  {
135      // Balance and number of transactions might have changed
136      fForceCheckBalanceChanged = true;
137  }
138  
139  void WalletModel::updateAddressBook(const QString &address, const QString &label,
140          bool isMine, wallet::AddressPurpose purpose, int status)
141  {
142      if(addressTableModel)
143          addressTableModel->updateEntry(address, label, isMine, purpose, status);
144  }
145  
146  bool WalletModel::validateAddress(const QString& address) const
147  {
148      return IsValidDestinationString(address.toStdString());
149  }
150  
151  WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction &transaction, const CCoinControl& coinControl)
152  {
153      transaction.getWtx() = nullptr; // reset tx output
154  
155      CAmount total = 0;
156      bool fSubtractFeeFromAmount = false;
157      QList<SendCoinsRecipient> recipients = transaction.getRecipients();
158      std::vector<CRecipient> vecSend;
159  
160      if(recipients.empty())
161      {
162          return OK;
163      }
164  
165      QSet<QString> setAddress; // Used to detect duplicates
166      int nAddresses = 0;
167  
168      // Pre-check input data for validity
169      for (const SendCoinsRecipient &rcp : recipients)
170      {
171          if (rcp.fSubtractFeeFromAmount)
172              fSubtractFeeFromAmount = true;
173          {   // User-entered bitcoin address / amount:
174              if(!validateAddress(rcp.address))
175              {
176                  return InvalidAddress;
177              }
178              if(rcp.amount <= 0)
179              {
180                  return InvalidAmount;
181              }
182              setAddress.insert(rcp.address);
183              ++nAddresses;
184  
185              vecSend.emplace_back(CRecipient{DecodeDestination(rcp.address.toStdString()), rcp.amount, rcp.fSubtractFeeFromAmount});
186  
187              total += rcp.amount;
188          }
189      }
190      if(setAddress.size() != nAddresses)
191      {
192          return DuplicateAddress;
193      }
194  
195      // If no coin was manually selected, use the cached balance
196      // Future: can merge this call with 'createTransaction'.
197      CAmount nBalance = getAvailableBalance(&coinControl);
198  
199      if(total > nBalance)
200      {
201          return AmountExceedsBalance;
202      }
203  
204      try {
205          auto& newTx = transaction.getWtx();
206          const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled(), /*change_pos=*/std::nullopt);
207          if (!res) {
208              Q_EMIT message(tr("Send Coins"), QString::fromStdString(util::ErrorString(res).translated),
209                             CClientUIInterface::MSG_ERROR);
210              return TransactionCreationFailed;
211          }
212  
213          newTx = res->tx;
214          CAmount nFeeRequired = res->fee;
215          transaction.setTransactionFee(nFeeRequired);
216          if (fSubtractFeeFromAmount && newTx) {
217              transaction.reassignAmounts(static_cast<int>(res->change_pos.value_or(-1)));
218          }
219  
220          // Reject absurdly high fee. (This can never happen because the
221          // wallet never creates transactions with fee greater than
222          // m_default_max_tx_fee. This merely a belt-and-suspenders check).
223          if (nFeeRequired > m_wallet->getDefaultMaxTxFee()) {
224              return AbsurdFee;
225          }
226      } catch (const std::runtime_error& err) {
227          // Something unexpected happened, instruct user to report this bug.
228          Q_EMIT message(tr("Send Coins"), QString::fromStdString(err.what()),
229                         CClientUIInterface::MSG_ERROR);
230          return TransactionCreationFailed;
231      }
232  
233      return SendCoinsReturn(OK);
234  }
235  
236  void WalletModel::sendCoins(WalletModelTransaction& transaction)
237  {
238      QByteArray transaction_array; /* store serialized transaction */
239  
240      {
241          std::vector<std::pair<std::string, std::string>> vOrderForm;
242          for (const SendCoinsRecipient &rcp : transaction.getRecipients())
243          {
244              if (!rcp.message.isEmpty()) // Message from normal bitcoin:URI (bitcoin:123...?message=example)
245                  vOrderForm.emplace_back("Message", rcp.message.toStdString());
246          }
247  
248          auto& newTx = transaction.getWtx();
249          wallet().commitTransaction(newTx, /*value_map=*/{}, std::move(vOrderForm));
250  
251          DataStream ssTx;
252          ssTx << TX_WITH_WITNESS(*newTx);
253          transaction_array.append((const char*)ssTx.data(), ssTx.size());
254      }
255  
256      // Add addresses / update labels that we've sent to the address book,
257      // and emit coinsSent signal for each recipient
258      for (const SendCoinsRecipient &rcp : transaction.getRecipients())
259      {
260          {
261              std::string strAddress = rcp.address.toStdString();
262              CTxDestination dest = DecodeDestination(strAddress);
263              std::string strLabel = rcp.label.toStdString();
264              {
265                  // Check if we have a new address or an updated label
266                  std::string name;
267                  if (!m_wallet->getAddress(
268                       dest, &name, /*purpose=*/nullptr))
269                  {
270                      m_wallet->setAddressBook(dest, strLabel, wallet::AddressPurpose::SEND);
271                  }
272                  else if (name != strLabel)
273                  {
274                      m_wallet->setAddressBook(dest, strLabel, {}); // {} means don't change purpose
275                  }
276              }
277          }
278          Q_EMIT coinsSent(this, rcp, transaction_array);
279      }
280  
281      checkBalanceChanged(m_wallet->getBalances()); // update balance immediately, otherwise there could be a short noticeable delay until pollBalanceChanged hits
282  }
283  
284  OptionsModel* WalletModel::getOptionsModel() const
285  {
286      return optionsModel;
287  }
288  
289  AddressTableModel* WalletModel::getAddressTableModel() const
290  {
291      return addressTableModel;
292  }
293  
294  TransactionTableModel* WalletModel::getTransactionTableModel() const
295  {
296      return transactionTableModel;
297  }
298  
299  RecentRequestsTableModel* WalletModel::getRecentRequestsTableModel() const
300  {
301      return recentRequestsTableModel;
302  }
303  
304  WalletModel::EncryptionStatus WalletModel::getEncryptionStatus() const
305  {
306      if(!m_wallet->isCrypted())
307      {
308          // A previous bug allowed for watchonly wallets to be encrypted (encryption keys set, but nothing is actually encrypted).
309          // To avoid misrepresenting the encryption status of such wallets, we only return NoKeys for watchonly wallets that are unencrypted.
310          if (m_wallet->privateKeysDisabled()) {
311              return NoKeys;
312          }
313          return Unencrypted;
314      }
315      else if(m_wallet->isLocked())
316      {
317          return Locked;
318      }
319      else
320      {
321          return Unlocked;
322      }
323  }
324  
325  bool WalletModel::setWalletEncrypted(const SecureString& passphrase)
326  {
327      return m_wallet->encryptWallet(passphrase);
328  }
329  
330  bool WalletModel::setWalletLocked(bool locked, const SecureString &passPhrase)
331  {
332      if(locked)
333      {
334          // Lock
335          return m_wallet->lock();
336      }
337      else
338      {
339          // Unlock
340          return m_wallet->unlock(passPhrase);
341      }
342  }
343  
344  bool WalletModel::changePassphrase(const SecureString &oldPass, const SecureString &newPass)
345  {
346      m_wallet->lock(); // Make sure wallet is locked before attempting pass change
347      return m_wallet->changeWalletPassphrase(oldPass, newPass);
348  }
349  
350  // Handlers for core signals
351  static void NotifyUnload(WalletModel* walletModel)
352  {
353      qDebug() << "NotifyUnload";
354      bool invoked = QMetaObject::invokeMethod(walletModel, "unload");
355      assert(invoked);
356  }
357  
358  static void NotifyKeyStoreStatusChanged(WalletModel *walletmodel)
359  {
360      qDebug() << "NotifyKeyStoreStatusChanged";
361      bool invoked = QMetaObject::invokeMethod(walletmodel, "updateStatus", Qt::QueuedConnection);
362      assert(invoked);
363  }
364  
365  static void NotifyAddressBookChanged(WalletModel *walletmodel,
366          const CTxDestination &address, const std::string &label, bool isMine,
367          wallet::AddressPurpose purpose, ChangeType status)
368  {
369      QString strAddress = QString::fromStdString(EncodeDestination(address));
370      QString strLabel = QString::fromStdString(label);
371  
372      qDebug() << "NotifyAddressBookChanged: " + strAddress + " " + strLabel + " isMine=" + QString::number(isMine) + " purpose=" + QString::number(static_cast<uint8_t>(purpose)) + " status=" + QString::number(status);
373      bool invoked = QMetaObject::invokeMethod(walletmodel, "updateAddressBook",
374                                Q_ARG(QString, strAddress),
375                                Q_ARG(QString, strLabel),
376                                Q_ARG(bool, isMine),
377                                Q_ARG(wallet::AddressPurpose, purpose),
378                                Q_ARG(int, status));
379      assert(invoked);
380  }
381  
382  static void NotifyTransactionChanged(WalletModel *walletmodel, const Txid& hash, ChangeType status)
383  {
384      Q_UNUSED(hash);
385      Q_UNUSED(status);
386      bool invoked = QMetaObject::invokeMethod(walletmodel, "updateTransaction", Qt::QueuedConnection);
387      assert(invoked);
388  }
389  
390  static void ShowProgress(WalletModel *walletmodel, const std::string &title, int nProgress)
391  {
392      // emits signal "showProgress"
393      bool invoked = QMetaObject::invokeMethod(walletmodel, "showProgress", Qt::QueuedConnection,
394                                Q_ARG(QString, QString::fromStdString(title)),
395                                Q_ARG(int, nProgress));
396      assert(invoked);
397  }
398  
399  static void NotifyCanGetAddressesChanged(WalletModel* walletmodel)
400  {
401      bool invoked = QMetaObject::invokeMethod(walletmodel, "canGetAddressesChanged");
402      assert(invoked);
403  }
404  
405  void WalletModel::subscribeToCoreSignals()
406  {
407      // Connect signals to wallet
408      m_handler_unload = m_wallet->handleUnload(std::bind_front(&NotifyUnload, this));
409      m_handler_status_changed = m_wallet->handleStatusChanged(std::bind_front(&NotifyKeyStoreStatusChanged, this));
410      m_handler_address_book_changed = m_wallet->handleAddressBookChanged(std::bind_front(NotifyAddressBookChanged, this));
411      m_handler_transaction_changed = m_wallet->handleTransactionChanged(std::bind_front(NotifyTransactionChanged, this));
412      m_handler_show_progress = m_wallet->handleShowProgress(std::bind_front(ShowProgress, this));
413      m_handler_can_get_addrs_changed = m_wallet->handleCanGetAddressesChanged(std::bind_front(NotifyCanGetAddressesChanged, this));
414  }
415  
416  void WalletModel::unsubscribeFromCoreSignals()
417  {
418      // Disconnect signals from wallet
419      m_handler_unload->disconnect();
420      m_handler_status_changed->disconnect();
421      m_handler_address_book_changed->disconnect();
422      m_handler_transaction_changed->disconnect();
423      m_handler_show_progress->disconnect();
424      m_handler_can_get_addrs_changed->disconnect();
425  }
426  
427  // WalletModel::UnlockContext implementation
428  WalletModel::UnlockContext WalletModel::requestUnlock()
429  {
430      // Bugs in earlier versions may have resulted in wallets with private keys disabled to become "encrypted"
431      // (encryption keys are present, but not actually doing anything).
432      // To avoid issues with such wallets, check if the wallet has private keys disabled, and if so, return a context
433      // that indicates the wallet is not encrypted.
434      if (m_wallet->privateKeysDisabled()) {
435          return UnlockContext(this, /*valid=*/true, /*relock=*/false);
436      }
437      bool was_locked = getEncryptionStatus() == Locked;
438      if(was_locked)
439      {
440          // Request UI to unlock wallet
441          Q_EMIT requireUnlock();
442      }
443      // If wallet is still locked, unlock was failed or cancelled, mark context as invalid
444      bool valid = getEncryptionStatus() != Locked;
445  
446      return UnlockContext(this, valid, was_locked);
447  }
448  
449  WalletModel::UnlockContext::UnlockContext(WalletModel *_wallet, bool _valid, bool _relock):
450          wallet(_wallet),
451          valid(_valid),
452          relock(_relock)
453  {
454  }
455  
456  WalletModel::UnlockContext::~UnlockContext()
457  {
458      if(valid && relock)
459      {
460          wallet->setWalletLocked(true);
461      }
462  }
463  
464  bool WalletModel::bumpFee(Txid hash, Txid& new_hash)
465  {
466      CCoinControl coin_control;
467      coin_control.m_signal_bip125_rbf = true;
468      std::vector<bilingual_str> errors;
469      CAmount old_fee;
470      CAmount new_fee;
471      CMutableTransaction mtx;
472      if (!m_wallet->createBumpTransaction(hash, coin_control, errors, old_fee, new_fee, mtx)) {
473          QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Increasing transaction fee failed") + "<br />(" +
474              (errors.size() ? QString::fromStdString(errors[0].translated) : "") +")");
475          return false;
476      }
477  
478      // allow a user based fee verification
479      /*: Asks a user if they would like to manually increase the fee of a transaction that has already been created. */
480      QString questionString = tr("Do you want to increase the fee?");
481      questionString.append("<br />");
482      questionString.append("<table style=\"text-align: left;\">");
483      questionString.append("<tr><td>");
484      questionString.append(tr("Current fee:"));
485      questionString.append("</td><td>");
486      questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), old_fee));
487      questionString.append("</td></tr><tr><td>");
488      questionString.append(tr("Increase:"));
489      questionString.append("</td><td>");
490      questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), new_fee - old_fee));
491      questionString.append("</td></tr><tr><td>");
492      questionString.append(tr("New fee:"));
493      questionString.append("</td><td>");
494      questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), new_fee));
495      questionString.append("</td></tr></table>");
496  
497      // Display warning in the "Confirm fee bump" window if the "Coin Control Features" option is enabled
498      if (getOptionsModel()->getCoinControlFeatures()) {
499          questionString.append("<br><br>");
500          questionString.append(tr("Warning: This may pay the additional fee by reducing change outputs or adding inputs, when necessary. It may add a new change output if one does not already exist. These changes may potentially leak privacy."));
501      }
502  
503      const bool enable_send{!wallet().privateKeysDisabled() || wallet().hasExternalSigner()};
504      const bool always_show_unsigned{getOptionsModel()->getEnablePSBTControls()};
505      auto confirmationDialog = new SendConfirmationDialog(tr("Confirm fee bump"), questionString, "", "", SEND_CONFIRM_DELAY, enable_send, always_show_unsigned, nullptr);
506      confirmationDialog->setAttribute(Qt::WA_DeleteOnClose);
507      // TODO: Replace QDialog::exec() with safer QDialog::show().
508      const auto retval = static_cast<QMessageBox::StandardButton>(confirmationDialog->exec());
509  
510      // cancel sign&broadcast if user doesn't want to bump the fee
511      if (retval != QMessageBox::Yes && retval != QMessageBox::Save) {
512          return false;
513      }
514  
515      // Short-circuit if we are returning a bumped transaction PSBT to clipboard
516      if (retval == QMessageBox::Save) {
517          // "Create Unsigned" clicked
518          PartiallySignedTransaction psbtx(mtx);
519          bool complete = false;
520          const auto err{wallet().fillPSBT({.sign = false, .bip32_derivs = true}, nullptr, psbtx, complete)};
521          if (err || complete) {
522              QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Can't draft transaction."));
523              return false;
524          }
525          // Serialize the PSBT
526          DataStream ssTx{};
527          ssTx << psbtx;
528          GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
529          Q_EMIT message(tr("PSBT copied"), tr("Fee-bump PSBT copied to clipboard"), CClientUIInterface::MSG_INFORMATION | CClientUIInterface::MODAL);
530          return true;
531      }
532  
533      WalletModel::UnlockContext ctx(requestUnlock());
534      if (!ctx.isValid()) {
535          return false;
536      }
537  
538      assert(!m_wallet->privateKeysDisabled() || wallet().hasExternalSigner());
539  
540      // sign bumped transaction
541      if (!m_wallet->signBumpTransaction(mtx)) {
542          QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Can't sign transaction."));
543          return false;
544      }
545      // commit the bumped transaction
546      if(!m_wallet->commitBumpTransaction(hash, std::move(mtx), errors, new_hash)) {
547          QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Could not commit transaction") + "<br />(" +
548              QString::fromStdString(errors[0].translated)+")");
549          return false;
550      }
551      return true;
552  }
553  
554  void WalletModel::displayAddress(std::string sAddress) const
555  {
556      CTxDestination dest = DecodeDestination(sAddress);
557      try {
558          util::Result<void> result = m_wallet->displayAddress(dest);
559          if (!result) {
560              QMessageBox::warning(nullptr, tr("Signer error"), QString::fromStdString(util::ErrorString(result).translated));
561          }
562      } catch (const std::runtime_error& e) {
563          QMessageBox::critical(nullptr, tr("Can't display address"), e.what());
564      }
565  }
566  
567  bool WalletModel::isWalletEnabled()
568  {
569     return !gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET);
570  }
571  
572  QString WalletModel::getWalletName() const
573  {
574      return QString::fromStdString(m_wallet->getWalletName());
575  }
576  
577  QString WalletModel::getDisplayName() const
578  {
579      return GUIUtil::WalletDisplayName(getWalletName());
580  }
581  
582  bool WalletModel::isMultiwallet() const
583  {
584      return m_node.walletLoader().getWallets().size() > 1;
585  }
586  
587  void WalletModel::refresh(bool pk_hash_only)
588  {
589      addressTableModel = new AddressTableModel(this, pk_hash_only);
590  }
591  
592  uint256 WalletModel::getLastBlockProcessed() const
593  {
594      return m_client_model ? m_client_model->getBestBlockHash() : uint256{};
595  }
596  
597  CAmount WalletModel::getAvailableBalance(const CCoinControl* control)
598  {
599      // No selected coins, return the cached balance
600      if (!control || !control->HasSelected()) {
601          const interfaces::WalletBalances& balances = getCachedBalance();
602          return balances.balance;
603      }
604      // Fetch balance from the wallet, taking into account the selected coins
605      return wallet().getAvailableBalance(*control);
606  }