// Copyright (c) 2011-2022 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include // for GetBoolArg #include #include #include #include #include #include #include #include // for CRecipient #include #include #include #include #include #include using wallet::CCoinControl; using wallet::CRecipient; using wallet::DEFAULT_DISABLE_WALLET; WalletModel::WalletModel(std::unique_ptr wallet, ClientModel& client_model, const PlatformStyle *platformStyle, QObject *parent) : QObject(parent), m_wallet(std::move(wallet)), m_client_model(&client_model), m_node(client_model.node()), optionsModel(client_model.getOptionsModel()), timer(new QTimer(this)) { fHaveWatchOnly = m_wallet->haveWatchOnly(); addressTableModel = new AddressTableModel(this); transactionTableModel = new TransactionTableModel(platformStyle, this); recentRequestsTableModel = new RecentRequestsTableModel(this); subscribeToCoreSignals(); } WalletModel::~WalletModel() { unsubscribeFromCoreSignals(); } void WalletModel::startPollBalance() { // Update the cached balance right away, so every view can make use of it, // so them don't need to waste resources recalculating it. pollBalanceChanged(); // This timer will be fired repeatedly to update the balance // Since the QTimer::timeout is a private signal, it cannot be used // in the GUIUtil::ExceptionSafeConnect directly. connect(timer, &QTimer::timeout, this, &WalletModel::timerTimeout); GUIUtil::ExceptionSafeConnect(this, &WalletModel::timerTimeout, this, &WalletModel::pollBalanceChanged); timer->start(MODEL_UPDATE_DELAY); } void WalletModel::setClientModel(ClientModel* client_model) { m_client_model = client_model; if (!m_client_model) timer->stop(); } void WalletModel::updateStatus() { EncryptionStatus newEncryptionStatus = getEncryptionStatus(); if(cachedEncryptionStatus != newEncryptionStatus) { Q_EMIT encryptionStatusChanged(); } } void WalletModel::pollBalanceChanged() { // Avoid recomputing wallet balances unless a TransactionChanged or // BlockTip notification was received. if (!fForceCheckBalanceChanged && m_cached_last_update_tip == getLastBlockProcessed()) return; // Try to get balances and return early if locks can't be acquired. This // avoids the GUI from getting stuck on periodical polls if the core is // holding the locks for a longer time - for example, during a wallet // rescan. interfaces::WalletBalances new_balances; uint256 block_hash; if (!m_wallet->tryGetBalances(new_balances, block_hash)) { return; } if (fForceCheckBalanceChanged || block_hash != m_cached_last_update_tip) { fForceCheckBalanceChanged = false; // Balance and number of transactions might have changed m_cached_last_update_tip = block_hash; checkBalanceChanged(new_balances); if(transactionTableModel) transactionTableModel->updateConfirmations(); } } void WalletModel::checkBalanceChanged(const interfaces::WalletBalances& new_balances) { if (new_balances.balanceChanged(m_cached_balances)) { m_cached_balances = new_balances; Q_EMIT balanceChanged(new_balances); } } interfaces::WalletBalances WalletModel::getCachedBalance() const { return m_cached_balances; } void WalletModel::updateTransaction() { // Balance and number of transactions might have changed fForceCheckBalanceChanged = true; } void WalletModel::updateAddressBook(const QString &address, const QString &label, bool isMine, wallet::AddressPurpose purpose, int status) { if(addressTableModel) addressTableModel->updateEntry(address, label, isMine, purpose, status); } void WalletModel::updateWatchOnlyFlag(bool fHaveWatchonly) { fHaveWatchOnly = fHaveWatchonly; Q_EMIT notifyWatchonlyChanged(fHaveWatchonly); } bool WalletModel::validateAddress(const QString& address) const { return IsValidDestinationString(address.toStdString()); } WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction &transaction, const CCoinControl& coinControl) { CAmount total = 0; bool fSubtractFeeFromAmount = false; QList recipients = transaction.getRecipients(); std::vector vecSend; if(recipients.empty()) { return OK; } QSet setAddress; // Used to detect duplicates int nAddresses = 0; // Pre-check input data for validity for (const SendCoinsRecipient &rcp : recipients) { if (rcp.fSubtractFeeFromAmount) fSubtractFeeFromAmount = true; { // User-entered bitcoin address / amount: if(!validateAddress(rcp.address)) { return InvalidAddress; } if(rcp.amount <= 0) { return InvalidAmount; } setAddress.insert(rcp.address); ++nAddresses; CRecipient recipient{DecodeDestination(rcp.address.toStdString()), rcp.amount, rcp.fSubtractFeeFromAmount}; vecSend.push_back(recipient); total += rcp.amount; } } if(setAddress.size() != nAddresses) { return DuplicateAddress; } // If no coin was manually selected, use the cached balance // Future: can merge this call with 'createTransaction'. CAmount nBalance = getAvailableBalance(&coinControl); if(total > nBalance) { return AmountExceedsBalance; } try { CAmount nFeeRequired = 0; int nChangePosRet = -1; auto& newTx = transaction.getWtx(); const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled(), nChangePosRet, nFeeRequired); newTx = res ? *res : nullptr; transaction.setTransactionFee(nFeeRequired); if (fSubtractFeeFromAmount && newTx) transaction.reassignAmounts(nChangePosRet); if(!newTx) { if(!fSubtractFeeFromAmount && (total + nFeeRequired) > nBalance) { return SendCoinsReturn(AmountWithFeeExceedsBalance); } Q_EMIT message(tr("Send Coins"), QString::fromStdString(util::ErrorString(res).translated), CClientUIInterface::MSG_ERROR); return TransactionCreationFailed; } // Reject absurdly high fee. (This can never happen because the // wallet never creates transactions with fee greater than // m_default_max_tx_fee. This merely a belt-and-suspenders check). if (nFeeRequired > m_wallet->getDefaultMaxTxFee()) { return AbsurdFee; } } catch (const std::runtime_error& err) { // Something unexpected happened, instruct user to report this bug. Q_EMIT message(tr("Send Coins"), QString::fromStdString(err.what()), CClientUIInterface::MSG_ERROR); return TransactionCreationFailed; } return SendCoinsReturn(OK); } void WalletModel::sendCoins(WalletModelTransaction& transaction) { QByteArray transaction_array; /* store serialized transaction */ { std::vector> vOrderForm; for (const SendCoinsRecipient &rcp : transaction.getRecipients()) { if (!rcp.message.isEmpty()) // Message from normal bitcoin:URI (bitcoin:123...?message=example) vOrderForm.emplace_back("Message", rcp.message.toStdString()); } auto& newTx = transaction.getWtx(); wallet().commitTransaction(newTx, /*value_map=*/{}, std::move(vOrderForm)); DataStream ssTx; ssTx << TX_WITH_WITNESS(*newTx); transaction_array.append((const char*)ssTx.data(), ssTx.size()); } // Add addresses / update labels that we've sent to the address book, // and emit coinsSent signal for each recipient for (const SendCoinsRecipient &rcp : transaction.getRecipients()) { { std::string strAddress = rcp.address.toStdString(); CTxDestination dest = DecodeDestination(strAddress); std::string strLabel = rcp.label.toStdString(); { // Check if we have a new address or an updated label std::string name; if (!m_wallet->getAddress( dest, &name, /* is_mine= */ nullptr, /* purpose= */ nullptr)) { m_wallet->setAddressBook(dest, strLabel, wallet::AddressPurpose::SEND); } else if (name != strLabel) { m_wallet->setAddressBook(dest, strLabel, {}); // {} means don't change purpose } } } Q_EMIT coinsSent(this, rcp, transaction_array); } checkBalanceChanged(m_wallet->getBalances()); // update balance immediately, otherwise there could be a short noticeable delay until pollBalanceChanged hits } OptionsModel* WalletModel::getOptionsModel() const { return optionsModel; } AddressTableModel* WalletModel::getAddressTableModel() const { return addressTableModel; } TransactionTableModel* WalletModel::getTransactionTableModel() const { return transactionTableModel; } RecentRequestsTableModel* WalletModel::getRecentRequestsTableModel() const { return recentRequestsTableModel; } WalletModel::EncryptionStatus WalletModel::getEncryptionStatus() const { if(!m_wallet->isCrypted()) { // A previous bug allowed for watchonly wallets to be encrypted (encryption keys set, but nothing is actually encrypted). // To avoid misrepresenting the encryption status of such wallets, we only return NoKeys for watchonly wallets that are unencrypted. if (m_wallet->privateKeysDisabled()) { return NoKeys; } return Unencrypted; } else if(m_wallet->isLocked()) { return Locked; } else { return Unlocked; } } bool WalletModel::setWalletEncrypted(const SecureString& passphrase) { return m_wallet->encryptWallet(passphrase); } bool WalletModel::setWalletLocked(bool locked, const SecureString &passPhrase) { if(locked) { // Lock return m_wallet->lock(); } else { // Unlock return m_wallet->unlock(passPhrase); } } bool WalletModel::changePassphrase(const SecureString &oldPass, const SecureString &newPass) { m_wallet->lock(); // Make sure wallet is locked before attempting pass change return m_wallet->changeWalletPassphrase(oldPass, newPass); } // Handlers for core signals static void NotifyUnload(WalletModel* walletModel) { qDebug() << "NotifyUnload"; bool invoked = QMetaObject::invokeMethod(walletModel, "unload"); assert(invoked); } static void NotifyKeyStoreStatusChanged(WalletModel *walletmodel) { qDebug() << "NotifyKeyStoreStatusChanged"; bool invoked = QMetaObject::invokeMethod(walletmodel, "updateStatus", Qt::QueuedConnection); assert(invoked); } static void NotifyAddressBookChanged(WalletModel *walletmodel, const CTxDestination &address, const std::string &label, bool isMine, wallet::AddressPurpose purpose, ChangeType status) { QString strAddress = QString::fromStdString(EncodeDestination(address)); QString strLabel = QString::fromStdString(label); qDebug() << "NotifyAddressBookChanged: " + strAddress + " " + strLabel + " isMine=" + QString::number(isMine) + " purpose=" + QString::number(static_cast(purpose)) + " status=" + QString::number(status); bool invoked = QMetaObject::invokeMethod(walletmodel, "updateAddressBook", Q_ARG(QString, strAddress), Q_ARG(QString, strLabel), Q_ARG(bool, isMine), Q_ARG(wallet::AddressPurpose, purpose), Q_ARG(int, status)); assert(invoked); } static void NotifyTransactionChanged(WalletModel *walletmodel, const uint256 &hash, ChangeType status) { Q_UNUSED(hash); Q_UNUSED(status); bool invoked = QMetaObject::invokeMethod(walletmodel, "updateTransaction", Qt::QueuedConnection); assert(invoked); } static void ShowProgress(WalletModel *walletmodel, const std::string &title, int nProgress) { // emits signal "showProgress" bool invoked = QMetaObject::invokeMethod(walletmodel, "showProgress", Qt::QueuedConnection, Q_ARG(QString, QString::fromStdString(title)), Q_ARG(int, nProgress)); assert(invoked); } static void NotifyWatchonlyChanged(WalletModel *walletmodel, bool fHaveWatchonly) { bool invoked = QMetaObject::invokeMethod(walletmodel, "updateWatchOnlyFlag", Qt::QueuedConnection, Q_ARG(bool, fHaveWatchonly)); assert(invoked); } static void NotifyCanGetAddressesChanged(WalletModel* walletmodel) { bool invoked = QMetaObject::invokeMethod(walletmodel, "canGetAddressesChanged"); assert(invoked); } void WalletModel::subscribeToCoreSignals() { // Connect signals to wallet m_handler_unload = m_wallet->handleUnload(std::bind(&NotifyUnload, this)); m_handler_status_changed = m_wallet->handleStatusChanged(std::bind(&NotifyKeyStoreStatusChanged, this)); m_handler_address_book_changed = m_wallet->handleAddressBookChanged(std::bind(NotifyAddressBookChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); m_handler_transaction_changed = m_wallet->handleTransactionChanged(std::bind(NotifyTransactionChanged, this, std::placeholders::_1, std::placeholders::_2)); m_handler_show_progress = m_wallet->handleShowProgress(std::bind(ShowProgress, this, std::placeholders::_1, std::placeholders::_2)); m_handler_watch_only_changed = m_wallet->handleWatchOnlyChanged(std::bind(NotifyWatchonlyChanged, this, std::placeholders::_1)); m_handler_can_get_addrs_changed = m_wallet->handleCanGetAddressesChanged(std::bind(NotifyCanGetAddressesChanged, this)); } void WalletModel::unsubscribeFromCoreSignals() { // Disconnect signals from wallet m_handler_unload->disconnect(); m_handler_status_changed->disconnect(); m_handler_address_book_changed->disconnect(); m_handler_transaction_changed->disconnect(); m_handler_show_progress->disconnect(); m_handler_watch_only_changed->disconnect(); m_handler_can_get_addrs_changed->disconnect(); } // WalletModel::UnlockContext implementation WalletModel::UnlockContext WalletModel::requestUnlock() { // Bugs in earlier versions may have resulted in wallets with private keys disabled to become "encrypted" // (encryption keys are present, but not actually doing anything). // To avoid issues with such wallets, check if the wallet has private keys disabled, and if so, return a context // that indicates the wallet is not encrypted. if (m_wallet->privateKeysDisabled()) { return UnlockContext(this, /*valid=*/true, /*relock=*/false); } bool was_locked = getEncryptionStatus() == Locked; if(was_locked) { // Request UI to unlock wallet Q_EMIT requireUnlock(); } // If wallet is still locked, unlock was failed or cancelled, mark context as invalid bool valid = getEncryptionStatus() != Locked; return UnlockContext(this, valid, was_locked); } WalletModel::UnlockContext::UnlockContext(WalletModel *_wallet, bool _valid, bool _relock): wallet(_wallet), valid(_valid), relock(_relock) { } WalletModel::UnlockContext::~UnlockContext() { if(valid && relock) { wallet->setWalletLocked(true); } } bool WalletModel::bumpFee(uint256 hash, uint256& new_hash) { CCoinControl coin_control; coin_control.m_signal_bip125_rbf = true; std::vector errors; CAmount old_fee; CAmount new_fee; CMutableTransaction mtx; if (!m_wallet->createBumpTransaction(hash, coin_control, errors, old_fee, new_fee, mtx)) { QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Increasing transaction fee failed") + "
(" + (errors.size() ? QString::fromStdString(errors[0].translated) : "") +")"); return false; } // allow a user based fee verification /*: Asks a user if they would like to manually increase the fee of a transaction that has already been created. */ QString questionString = tr("Do you want to increase the fee?"); questionString.append("
"); questionString.append(""); questionString.append("
"); questionString.append(tr("Current fee:")); questionString.append(""); questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), old_fee)); questionString.append("
"); questionString.append(tr("Increase:")); questionString.append(""); questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), new_fee - old_fee)); questionString.append("
"); questionString.append(tr("New fee:")); questionString.append(""); questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), new_fee)); questionString.append("
"); // Display warning in the "Confirm fee bump" window if the "Coin Control Features" option is enabled if (getOptionsModel()->getCoinControlFeatures()) { questionString.append("

"); 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.")); } const bool enable_send{!wallet().privateKeysDisabled() || wallet().hasExternalSigner()}; const bool always_show_unsigned{getOptionsModel()->getEnablePSBTControls()}; auto confirmationDialog = new SendConfirmationDialog(tr("Confirm fee bump"), questionString, "", "", SEND_CONFIRM_DELAY, enable_send, always_show_unsigned, nullptr); confirmationDialog->setAttribute(Qt::WA_DeleteOnClose); // TODO: Replace QDialog::exec() with safer QDialog::show(). const auto retval = static_cast(confirmationDialog->exec()); // cancel sign&broadcast if user doesn't want to bump the fee if (retval != QMessageBox::Yes && retval != QMessageBox::Save) { return false; } WalletModel::UnlockContext ctx(requestUnlock()); if(!ctx.isValid()) { return false; } // Short-circuit if we are returning a bumped transaction PSBT to clipboard if (retval == QMessageBox::Save) { // "Create Unsigned" clicked PartiallySignedTransaction psbtx(mtx); bool complete = false; const TransactionError err = wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, nullptr, psbtx, complete); if (err != TransactionError::OK || complete) { QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Can't draft transaction.")); return false; } // Serialize the PSBT DataStream ssTx{}; ssTx << psbtx; GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str()); Q_EMIT message(tr("PSBT copied"), tr("Copied to clipboard", "Fee-bump PSBT saved"), CClientUIInterface::MSG_INFORMATION); return true; } assert(!m_wallet->privateKeysDisabled() || wallet().hasExternalSigner()); // sign bumped transaction if (!m_wallet->signBumpTransaction(mtx)) { QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Can't sign transaction.")); return false; } // commit the bumped transaction if(!m_wallet->commitBumpTransaction(hash, std::move(mtx), errors, new_hash)) { QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Could not commit transaction") + "
(" + QString::fromStdString(errors[0].translated)+")"); return false; } return true; } void WalletModel::displayAddress(std::string sAddress) const { CTxDestination dest = DecodeDestination(sAddress); try { util::Result result = m_wallet->displayAddress(dest); if (!result) { QMessageBox::warning(nullptr, tr("Signer error"), QString::fromStdString(util::ErrorString(result).translated)); } } catch (const std::runtime_error& e) { QMessageBox::critical(nullptr, tr("Can't display address"), e.what()); } } bool WalletModel::isWalletEnabled() { return !gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET); } QString WalletModel::getWalletName() const { return QString::fromStdString(m_wallet->getWalletName()); } QString WalletModel::getDisplayName() const { const QString name = getWalletName(); return name.isEmpty() ? "["+tr("default wallet")+"]" : name; } bool WalletModel::isMultiwallet() const { return m_node.walletLoader().getWallets().size() > 1; } void WalletModel::refresh(bool pk_hash_only) { addressTableModel = new AddressTableModel(this, pk_hash_only); } uint256 WalletModel::getLastBlockProcessed() const { return m_client_model ? m_client_model->getBestBlockHash() : uint256{}; } CAmount WalletModel::getAvailableBalance(const CCoinControl* control) { // No selected coins, return the cached balance if (!control || !control->HasSelected()) { const interfaces::WalletBalances& balances = getCachedBalance(); CAmount available_balance = balances.balance; // if wallet private keys are disabled, this is a watch-only wallet // so, let's include the watch-only balance. if (balances.have_watch_only && m_wallet->privateKeysDisabled()) { available_balance += balances.watch_only_balance; } return available_balance; } // Fetch balance from the wallet, taking into account the selected coins return wallet().getAvailableBalance(*control); }