0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-24 12:41:41 -05:00
bitcoin-bitcoin-core/src/qt/walletmodel.cpp
Ryan Ofsky 02e62c6c9a common: Add PSBTError enum
Add separate PSBTError enum instead of reusing TransactionError enum for PSBT
operations, and drop unused error codes. The error codes returned by PSBT
operations and transaction broadcast functions mostly do not overlap, so using
an unified enum makes it harder to call any of these functions and know which
errors actually need to be handled.

Define PSBTError in the common library because PSBT functionality is
implemented in the common library and used by both the node (for rawtransaction
RPCs) and the wallet.
2024-05-16 10:16:08 -05:00

630 lines
22 KiB
C++

// 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 <qt/walletmodel.h>
#include <qt/addresstablemodel.h>
#include <qt/clientmodel.h>
#include <qt/guiconstants.h>
#include <qt/guiutil.h>
#include <qt/optionsmodel.h>
#include <qt/paymentserver.h>
#include <qt/recentrequeststablemodel.h>
#include <qt/sendcoinsdialog.h>
#include <qt/transactiontablemodel.h>
#include <common/args.h> // for GetBoolArg
#include <interfaces/handler.h>
#include <interfaces/node.h>
#include <key_io.h>
#include <node/interface_ui.h>
#include <psbt.h>
#include <util/translation.h>
#include <wallet/coincontrol.h>
#include <wallet/wallet.h> // for CRecipient
#include <stdint.h>
#include <functional>
#include <QDebug>
#include <QMessageBox>
#include <QSet>
#include <QTimer>
using wallet::CCoinControl;
using wallet::CRecipient;
using wallet::DEFAULT_DISABLE_WALLET;
WalletModel::WalletModel(std::unique_ptr<interfaces::Wallet> 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<SendCoinsRecipient> recipients = transaction.getRecipients();
std::vector<CRecipient> vecSend;
if(recipients.empty())
{
return OK;
}
QSet<QString> 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<std::pair<std::string, std::string>> 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<uint8_t>(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<bilingual_str> 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") + "<br />(" +
(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("<br />");
questionString.append("<table style=\"text-align: left;\">");
questionString.append("<tr><td>");
questionString.append(tr("Current fee:"));
questionString.append("</td><td>");
questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), old_fee));
questionString.append("</td></tr><tr><td>");
questionString.append(tr("Increase:"));
questionString.append("</td><td>");
questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), new_fee - old_fee));
questionString.append("</td></tr><tr><td>");
questionString.append(tr("New fee:"));
questionString.append("</td><td>");
questionString.append(BitcoinUnits::formatHtmlWithUnit(getOptionsModel()->getDisplayUnit(), new_fee));
questionString.append("</td></tr></table>");
// Display warning in the "Confirm fee bump" window if the "Coin Control Features" option is enabled
if (getOptionsModel()->getCoinControlFeatures()) {
questionString.append("<br><br>");
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<QMessageBox::StandardButton>(confirmationDialog->exec());
// cancel sign&broadcast if user doesn't want to bump the fee
if (retval != QMessageBox::Yes && retval != QMessageBox::Save) {
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 auto err{wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, nullptr, psbtx, complete)};
if (err || 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("Fee-bump PSBT copied to clipboard"), CClientUIInterface::MSG_INFORMATION | CClientUIInterface::MODAL);
return true;
}
WalletModel::UnlockContext ctx(requestUnlock());
if (!ctx.isValid()) {
return false;
}
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") + "<br />(" +
QString::fromStdString(errors[0].translated)+")");
return false;
}
return true;
}
void WalletModel::displayAddress(std::string sAddress) const
{
CTxDestination dest = DecodeDestination(sAddress);
try {
util::Result<void> 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);
}