diff --git a/doc/release-notes.md b/doc/release-notes.md index c2f5e5d21..145a08e85 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -1,5 +1,13 @@ Bitcoin ABC version 0.21.9 is now available from: This release includes the following features and fixes: +- Improve management of maxfee by the wallet. + +Wallet changes +-------------- +When creating a transaction with a fee above `-maxtxfee` (default 0.1 BCH), +the RPC commands `walletcreatefundedpsbt` and `fundrawtransaction` will now fail +instead of rounding down the fee. Beware that the `feeRate` argument is specified +in BCH per kilobyte, not satoshi per byte. diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index c5b1fc98f..aa75d74e7 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -1,535 +1,533 @@ // Copyright (c) 2011-2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #if defined(HAVE_CONFIG_H) #include #endif #include #include #include #include #include #include #include #include #include #include #include #include // for GetBoolArg #include #include #include #include #include #include WalletModel::WalletModel(std::unique_ptr wallet, interfaces::Node &node, const PlatformStyle *platformStyle, OptionsModel *_optionsModel, QObject *parent) : QObject(parent), m_wallet(std::move(wallet)), m_node(node), optionsModel(_optionsModel), addressTableModel(nullptr), transactionTableModel(nullptr), recentRequestsTableModel(nullptr), cachedEncryptionStatus(Unencrypted), cachedNumBlocks(0) { fHaveWatchOnly = m_wallet->haveWatchOnly(); addressTableModel = new AddressTableModel(this); transactionTableModel = new TransactionTableModel(platformStyle, this); recentRequestsTableModel = new RecentRequestsTableModel(this); // This timer will be fired repeatedly to update the balance pollTimer = new QTimer(this); connect(pollTimer, &QTimer::timeout, this, &WalletModel::pollBalanceChanged); pollTimer->start(MODEL_UPDATE_DELAY); subscribeToCoreSignals(); } WalletModel::~WalletModel() { unsubscribeFromCoreSignals(); } void WalletModel::updateStatus() { EncryptionStatus newEncryptionStatus = getEncryptionStatus(); if (cachedEncryptionStatus != newEncryptionStatus) { Q_EMIT encryptionStatusChanged(); } } void WalletModel::pollBalanceChanged() { // 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; int numBlocks = -1; if (!m_wallet->tryGetBalances(new_balances, numBlocks)) { return; } if (fForceCheckBalanceChanged || m_node.getNumBlocks() != cachedNumBlocks) { fForceCheckBalanceChanged = false; // Balance and number of transactions might have changed cachedNumBlocks = m_node.getNumBlocks(); 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); } } void WalletModel::updateTransaction() { // Balance and number of transactions might have changed fForceCheckBalanceChanged = true; } void WalletModel::updateAddressBook(const QString &address, const QString &label, bool isMine, const QString &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) { return IsValidDestinationString(address.toStdString(), getChainParams()); } WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransaction &transaction, const CCoinControl &coinControl) { Amount total = Amount::zero(); bool fSubtractFeeFromAmount = false; QList recipients = transaction.getRecipients(); std::vector vecSend; if (recipients.empty()) { return OK; } // Used to detect duplicates QSet setAddress; int nAddresses = 0; // Pre-check input data for validity for (const SendCoinsRecipient &rcp : recipients) { if (rcp.fSubtractFeeFromAmount) { fSubtractFeeFromAmount = true; } #ifdef ENABLE_BIP70 // PaymentRequest... if (rcp.paymentRequest.IsInitialized()) { Amount subtotal = Amount::zero(); const payments::PaymentDetails &details = rcp.paymentRequest.getDetails(); for (int i = 0; i < details.outputs_size(); i++) { const payments::Output &out = details.outputs(i); if (out.amount() <= 0) { continue; } subtotal += int64_t(out.amount()) * SATOSHI; const uint8_t *scriptStr = (const uint8_t *)out.script().data(); CScript scriptPubKey(scriptStr, scriptStr + out.script().size()); Amount nAmount = int64_t(out.amount()) * SATOSHI; CRecipient recipient = {scriptPubKey, nAmount, rcp.fSubtractFeeFromAmount}; vecSend.push_back(recipient); } if (subtotal <= Amount::zero()) { return InvalidAmount; } total += subtotal; } // User-entered bitcoin address / amount: else #endif { if (!validateAddress(rcp.address)) { return InvalidAddress; } if (rcp.amount <= Amount::zero()) { return InvalidAmount; } setAddress.insert(rcp.address); ++nAddresses; CScript scriptPubKey = GetScriptForDestination( DecodeDestination(rcp.address.toStdString(), getChainParams())); CRecipient recipient = {scriptPubKey, Amount(rcp.amount), rcp.fSubtractFeeFromAmount}; vecSend.push_back(recipient); total += rcp.amount; } } if (setAddress.size() != nAddresses) { return DuplicateAddress; } Amount nBalance = m_wallet->getAvailableBalance(coinControl); if (total > nBalance) { return AmountExceedsBalance; } Amount nFeeRequired = Amount::zero(); int nChangePosRet = -1; std::string strFailReason; auto &newTx = transaction.getWtx(); newTx = m_wallet->createTransaction(vecSend, coinControl, true /* sign */, nChangePosRet, nFeeRequired, strFailReason); 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(strFailReason), CClientUIInterface::MSG_ERROR); return TransactionCreationFailed; } - // reject absurdly high fee. (This can never happen because the wallet caps - // the fee at m_default_max_tx_fee. This merely serves as a - // belt-and-suspenders check) + // Reject absurdly high fee if (nFeeRequired > m_wallet->getDefaultMaxTxFee()) { return AbsurdFee; } return SendCoinsReturn(OK); } WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction &transaction) { /* store serialized transaction */ QByteArray transaction_array; std::vector> vOrderForm; for (const SendCoinsRecipient &rcp : transaction.getRecipients()) { #ifdef ENABLE_BIP70 if (rcp.paymentRequest.IsInitialized()) { // Make sure any payment requests involved are still valid. if (PaymentServer::verifyExpired(rcp.paymentRequest.getDetails())) { return PaymentRequestExpired; } // Store PaymentRequests in wtx.vOrderForm in wallet. std::string value; rcp.paymentRequest.SerializeToString(&value); vOrderForm.emplace_back("PaymentRequest", std::move(value)); } else #endif { if (!rcp.message.isEmpty()) { // Message from normal bitcoincash:URI // (bitcoincash:123...?message=example) vOrderForm.emplace_back("Message", rcp.message.toStdString()); } } } auto &newTx = transaction.getWtx(); std::string rejectReason; if (!newTx->commit({} /* mapValue */, std::move(vOrderForm), rejectReason)) { return SendCoinsReturn(TransactionCommitFailed, QString::fromStdString(rejectReason)); } CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); ssTx << newTx->get(); transaction_array.append(&(ssTx[0]), 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()) { // Don't touch the address book when we have a payment request #ifdef ENABLE_BIP70 if (!rcp.paymentRequest.IsInitialized()) #endif { std::string strAddress = rcp.address.toStdString(); CTxDestination dest = DecodeDestination(strAddress, getChainParams()); 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, "send"); } else if (name != strLabel) { // "" means don't change purpose m_wallet->setAddressBook(dest, strLabel, ""); } } Q_EMIT coinsSent(this, rcp, transaction_array); } // update balance immediately, otherwise there could be a short noticeable // delay until pollBalanceChanged hits checkBalanceChanged(m_wallet->getBalances()); return SendCoinsReturn(OK); } OptionsModel *WalletModel::getOptionsModel() { return optionsModel; } AddressTableModel *WalletModel::getAddressTableModel() { return addressTableModel; } TransactionTableModel *WalletModel::getTransactionTableModel() { return transactionTableModel; } RecentRequestsTableModel *WalletModel::getRecentRequestsTableModel() { return recentRequestsTableModel; } WalletModel::EncryptionStatus WalletModel::getEncryptionStatus() const { if (!m_wallet->isCrypted()) { return Unencrypted; } else if (m_wallet->isLocked()) { return Locked; } else { return Unlocked; } } bool WalletModel::setWalletEncrypted(bool encrypted, const SecureString &passphrase) { if (encrypted) { // Encrypt return m_wallet->encryptWallet(passphrase); } else { // Decrypt -- TODO; not supported yet return false; } } 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) { // Make sure wallet is locked before attempting pass change m_wallet->lock(); 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, const std::string &purpose, ChangeType status) { QString strAddress = QString::fromStdString( EncodeCashAddr(address, walletmodel->getChainParams())); QString strLabel = QString::fromStdString(label); QString strPurpose = QString::fromStdString(purpose); qDebug() << "NotifyAddressBookChanged: " + strAddress + " " + strLabel + " isMine=" + QString::number(isMine) + " purpose=" + strPurpose + " status=" + QString::number(status); bool invoked = QMetaObject::invokeMethod( walletmodel, "updateAddressBook", Qt::QueuedConnection, Q_ARG(QString, strAddress), Q_ARG(QString, strLabel), Q_ARG(bool, isMine), Q_ARG(QString, strPurpose), Q_ARG(int, status)); assert(invoked); } static void NotifyTransactionChanged(WalletModel *walletmodel, const TxId &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() { 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); } } void WalletModel::UnlockContext::CopyFrom(const UnlockContext &rhs) { // Transfer context; old object no longer relocks wallet *this = rhs; rhs.relock = false; } void WalletModel::loadReceiveRequests( std::vector &vReceiveRequests) { // receive request vReceiveRequests = m_wallet->getDestValues("rr"); } bool WalletModel::saveReceiveRequest(const std::string &sAddress, const int64_t nId, const std::string &sRequest) { CTxDestination dest = DecodeDestination(sAddress, getChainParams()); std::stringstream ss; ss << nId; // "rr" prefix = "receive request" in destdata std::string key = "rr" + ss.str(); return sRequest.empty() ? m_wallet->eraseDestData(dest, key) : m_wallet->addDestData(dest, key, sRequest); } bool WalletModel::isWalletEnabled() { return !gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET); } bool WalletModel::privateKeysDisabled() const { return m_wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); } bool WalletModel::canGetAddresses() const { return m_wallet->canGetAddresses(); } 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() { return m_node.getWallets().size() > 1; } const CChainParams &WalletModel::getChainParams() const { return Params(); } diff --git a/src/util/error.cpp b/src/util/error.cpp index 01dc5f4ab..2537807a2 100644 --- a/src/util/error.cpp +++ b/src/util/error.cpp @@ -1,43 +1,46 @@ // Copyright (c) 2010-2018 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 std::string TransactionErrorString(const TransactionError error) { switch (error) { case TransactionError::OK: return "No error"; case TransactionError::MISSING_INPUTS: return "Missing inputs"; case TransactionError::ALREADY_IN_CHAIN: return "Transaction already in block chain"; case TransactionError::P2P_DISABLED: return "Peer-to-peer functionality missing or disabled"; case TransactionError::MEMPOOL_REJECTED: return "Transaction rejected by AcceptToMemoryPool"; case TransactionError::MEMPOOL_ERROR: return "AcceptToMemoryPool failed"; case TransactionError::INVALID_PSBT: return "PSBT is not sane"; case TransactionError::PSBT_MISMATCH: return "PSBTs not compatible (different transactions)"; case TransactionError::SIGHASH_MISMATCH: return "Specified sighash value does not match existing value"; - } // no default case, so the compiler can warn about missing cases + case TransactionError::MAX_FEE_EXCEEDED: + return "Fee exceeds maximum configured by -maxtxfee"; + // no default case, so the compiler can warn about missing cases + } assert(false); } std::string AmountHighWarn(const std::string &optname) { return strprintf(_("%s is set very high!").translated, optname); } std::string AmountErrMsg(const std::string &optname, const std::string &strValue) { return strprintf(_("Invalid amount for -%s=: '%s'").translated, optname, strValue); } diff --git a/src/util/error.h b/src/util/error.h index 5c42c1e25..e8b1c9564 100644 --- a/src/util/error.h +++ b/src/util/error.h @@ -1,40 +1,41 @@ // Copyright (c) 2010-2018 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_UTIL_ERROR_H #define BITCOIN_UTIL_ERROR_H /** * util/error.h is a common place for definitions of simple error types and * string functions. Types and functions defined here should not require any * outside dependencies. * * Error types defined here can be used in different parts of the bitcoin * codebase, to avoid the need to write boilerplate code catching and * translating errors passed across wallet/node/rpc/gui code boundaries. */ #include enum class TransactionError { OK, //!< No error MISSING_INPUTS, ALREADY_IN_CHAIN, P2P_DISABLED, MEMPOOL_REJECTED, MEMPOOL_ERROR, INVALID_PSBT, PSBT_MISMATCH, SIGHASH_MISMATCH, + MAX_FEE_EXCEEDED, }; std::string TransactionErrorString(TransactionError error); std::string AmountHighWarn(const std::string &optname); std::string AmountErrMsg(const std::string &optname, const std::string &strValue); #endif // BITCOIN_UTIL_ERROR_H diff --git a/src/wallet/fees.cpp b/src/wallet/fees.cpp index 997886579..8aeb91b39 100644 --- a/src/wallet/fees.cpp +++ b/src/wallet/fees.cpp @@ -1,56 +1,47 @@ // Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2017 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 Amount GetRequiredFee(const CWallet &wallet, unsigned int nTxBytes) { return GetRequiredFeeRate(wallet).GetFeeCeiling(nTxBytes); } Amount GetMinimumFee(const CWallet &wallet, unsigned int nTxBytes, const CCoinControl &coin_control) { - Amount nFeeNeeded = - GetMinimumFeeRate(wallet, coin_control).GetFeeCeiling(nTxBytes); - - // But always obey the maximum. - const Amount max_tx_fee = wallet.m_default_max_tx_fee; - if (nFeeNeeded > max_tx_fee) { - nFeeNeeded = max_tx_fee; - } - - return nFeeNeeded; + return GetMinimumFeeRate(wallet, coin_control).GetFeeCeiling(nTxBytes); } CFeeRate GetRequiredFeeRate(const CWallet &wallet) { return std::max(wallet.m_min_fee, wallet.chain().relayMinFee()); } CFeeRate GetMinimumFeeRate(const CWallet &wallet, const CCoinControl &coin_control) { CFeeRate neededFeeRate = (coin_control.fOverrideFeeRate && coin_control.m_feerate) ? *coin_control.m_feerate : wallet.m_pay_tx_fee; if (neededFeeRate == CFeeRate()) { neededFeeRate = wallet.chain().estimateFee(); // ... unless we don't have enough mempool data for estimatefee, then // use fallback fee. if (neededFeeRate == CFeeRate()) { neededFeeRate = wallet.m_fallback_fee; } } // Prevent user from paying a fee below minRelayTxFee or minTxFee. return std::max(neededFeeRate, GetRequiredFeeRate(wallet)); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 21034d7dd..098382b6c 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1,5014 +1,5020 @@ // Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2016 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 #include #include #include #include #include #include #include #include #include