diff --git a/src/qt/forms/sendcoinsdialog.ui b/src/qt/forms/sendcoinsdialog.ui index 2dc126f8b..b6bf0700e 100644 --- a/src/qt/forms/sendcoinsdialog.ui +++ b/src/qt/forms/sendcoinsdialog.ui @@ -1,1192 +1,1173 @@ SendCoinsDialog 0 0 850 526 Send Coins 8 0 0 16777215 16777215 QFrame::StyledPanel QFrame::Sunken 0 0 0 0 6 0 10 10 15 0 0 75 true font-weight:bold; Coin Control Features 8 10 Inputs... false automatically selected 5 75 true color:red;font-weight:bold; Insufficient funds! 5 Qt::Horizontal 40 1 0 0 0 0 0 0 0 0 20 0 10 10 14 10 4 6 75 true Quantity: 0 IBeamCursor Qt::ActionsContextMenu 0 0 Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 75 true Bytes: IBeamCursor Qt::ActionsContextMenu 0 Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 10 14 6 4 6 75 true Amount: 0 IBeamCursor Qt::ActionsContextMenu 0.00 BCH Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 75 true Dust: IBeamCursor Qt::ActionsContextMenu no Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 10 14 6 4 6 75 true Fee: 0 IBeamCursor Qt::ActionsContextMenu 0.00 BCH Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 10 14 6 4 6 75 true After Fee: 0 IBeamCursor Qt::ActionsContextMenu 0.00 BCH Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 75 true Change: IBeamCursor Qt::ActionsContextMenu 0.00 BCH Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 12 QLayout::SetDefaultConstraint 5 5 If this is activated, but the change address is empty or invalid, change will be sent to a newly generated address. Custom change address false 0 0 0 0 0 0 3 Qt::Vertical 800 1 true 0 0 830 104 0 0 0 0 6 Qt::Vertical 20 40 0 0 16777215 16777215 QFrame::StyledPanel QFrame::Sunken 0 0 0 0 0 0 10 0 0 0 Qt::Vertical QSizePolicy::Fixed 1 4 10 0 0 75 true font-weight:bold; Transaction Fee: Choose... Qt::Vertical 1 1 Qt::Horizontal 40 20 collapse fee-settings Hide 0 0 0 0 0 10 4 10 4 6 Specify a custom fee per kB (1,000 bytes) of the transaction's size. Note: Since the fee is calculated on a per-byte basis, a fee of "100 satoshis per kB" for a transaction size of 500 bytes (half of 1 kB) would ultimately yield a fee of only 50 satoshis. per kilobyte Qt::Horizontal 1 1 - - - Paying only the minimum fee is just fine as long as there is less transaction volume than space in the blocks. But be aware that this can end up in a never confirming transaction once there is more demand for bitcoin transactions than the network can process. - - - - - - - - + true - Paying only the minimum fee is just fine as long as there is less transaction volume than space in the blocks. But be aware that this can end up in a never confirming transaction once there is more demand for bitcoin transactions than the network can process. + When there is less transaction volume than space in the blocks, miners as well as relaying nodes may enforce a minimum fee. Paying only this minimum fee is just fine, but be aware that this can result in a never confirming transaction once there is more demand for bitcoin transactions than the network can process. - (read the tooltip) - - - 5 + A too low fee might result in a never confirming transaction (read the tooltip) Qt::Horizontal 1 1 Recommended: true groupFee Qt::Vertical 1 1 Custom: groupFee Qt::Vertical 1 1 6 2 - - 2 - (Smart fee not initialized yet. This usually takes a few blocks...) - - 2 - Qt::Horizontal 1 1 Qt::Vertical 1 1 Qt::Vertical 800 1 150 0 Confirm the send action S&end :/icons/send:/icons/send false false 0 0 Clear all fields of the form. Clear &All :/icons/remove:/icons/remove false Send to multiple recipients at once Add &Recipient :/icons/add:/icons/add false Qt::Horizontal 40 20 3 0 0 Balance: 0 0 IBeamCursor 123.456 BCH Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse QValidatedLineEdit QLineEdit
qt/qvalidatedlineedit.h
BitcoinAmountField QLineEdit
qt/bitcoinamountfield.h
1
diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 05bac670f..edddbdc05 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -1,1069 +1,1041 @@ // Copyright (c) 2011-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. #if defined(HAVE_CONFIG_H) #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include SendCoinsDialog::SendCoinsDialog(const PlatformStyle *_platformStyle, WalletModel *_model, QWidget *parent) : QDialog(parent), ui(new Ui::SendCoinsDialog), clientModel(nullptr), model(_model), fNewRecipientAllowed(true), fFeeMinimized(true), platformStyle(_platformStyle) { ui->setupUi(this); if (!_platformStyle->getImagesOnButtons()) { ui->addButton->setIcon(QIcon()); ui->clearButton->setIcon(QIcon()); ui->sendButton->setIcon(QIcon()); } else { ui->addButton->setIcon(_platformStyle->SingleColorIcon(":/icons/add")); ui->clearButton->setIcon( _platformStyle->SingleColorIcon(":/icons/remove")); ui->sendButton->setIcon( _platformStyle->SingleColorIcon(":/icons/send")); } GUIUtil::setupAddressWidget(ui->lineEditCoinControlChange, this); addEntry(); connect(ui->addButton, &QPushButton::clicked, this, &SendCoinsDialog::addEntry); connect(ui->clearButton, &QPushButton::clicked, this, &SendCoinsDialog::clear); // Coin Control connect(ui->pushButtonCoinControl, &QPushButton::clicked, this, &SendCoinsDialog::coinControlButtonClicked); connect(ui->checkBoxCoinControlChange, &QCheckBox::stateChanged, this, &SendCoinsDialog::coinControlChangeChecked); connect(ui->lineEditCoinControlChange, &QValidatedLineEdit::textEdited, this, &SendCoinsDialog::coinControlChangeEdited); // Coin Control: clipboard actions QAction *clipboardQuantityAction = new QAction(tr("Copy quantity"), this); QAction *clipboardAmountAction = new QAction(tr("Copy amount"), this); QAction *clipboardFeeAction = new QAction(tr("Copy fee"), this); QAction *clipboardAfterFeeAction = new QAction(tr("Copy after fee"), this); QAction *clipboardBytesAction = new QAction(tr("Copy bytes"), this); QAction *clipboardLowOutputAction = new QAction(tr("Copy dust"), this); QAction *clipboardChangeAction = new QAction(tr("Copy change"), this); connect(clipboardQuantityAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardQuantity); connect(clipboardAmountAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardAmount); connect(clipboardFeeAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardFee); connect(clipboardAfterFeeAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardAfterFee); connect(clipboardBytesAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardBytes); connect(clipboardLowOutputAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardLowOutput); connect(clipboardChangeAction, &QAction::triggered, this, &SendCoinsDialog::coinControlClipboardChange); ui->labelCoinControlQuantity->addAction(clipboardQuantityAction); ui->labelCoinControlAmount->addAction(clipboardAmountAction); ui->labelCoinControlFee->addAction(clipboardFeeAction); ui->labelCoinControlAfterFee->addAction(clipboardAfterFeeAction); ui->labelCoinControlBytes->addAction(clipboardBytesAction); ui->labelCoinControlLowOutput->addAction(clipboardLowOutputAction); ui->labelCoinControlChange->addAction(clipboardChangeAction); // init transaction fee section QSettings settings; if (!settings.contains("fFeeSectionMinimized")) { settings.setValue("fFeeSectionMinimized", true); } // compatibility if (!settings.contains("nFeeRadio") && settings.contains("nTransactionFee") && settings.value("nTransactionFee").toLongLong() > 0) { // custom settings.setValue("nFeeRadio", 1); } if (!settings.contains("nFeeRadio")) { // recommended settings.setValue("nFeeRadio", 0); } if (!settings.contains("nTransactionFee")) { settings.setValue("nTransactionFee", qint64(DEFAULT_PAY_TX_FEE / SATOSHI)); } - if (!settings.contains("fPayOnlyMinFee")) { - settings.setValue("fPayOnlyMinFee", false); - } ui->groupFee->setId(ui->radioSmartFee, 0); ui->groupFee->setId(ui->radioCustomFee, 1); ui->groupFee ->button( std::max(0, std::min(1, settings.value("nFeeRadio").toInt()))) ->setChecked(true); + ui->customFee->SetAllowEmpty(false); ui->customFee->setValue( int64_t(settings.value("nTransactionFee").toLongLong()) * SATOSHI); - ui->checkBoxMinimumFee->setChecked( - settings.value("fPayOnlyMinFee").toBool()); minimizeFeeSection(settings.value("fFeeSectionMinimized").toBool()); // Set the model properly. setModel(model); } void SendCoinsDialog::setClientModel(ClientModel *_clientModel) { this->clientModel = _clientModel; if (_clientModel) { connect(_clientModel, &ClientModel::numBlocksChanged, this, &SendCoinsDialog::updateSmartFeeLabel); } } void SendCoinsDialog::setModel(WalletModel *_model) { this->model = _model; if (_model && _model->getOptionsModel()) { for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast( ui->entries->itemAt(i)->widget()); if (entry) { entry->setModel(_model); } } interfaces::WalletBalances balances = _model->wallet().getBalances(); setBalance(balances); connect(_model, &WalletModel::balanceChanged, this, &SendCoinsDialog::setBalance); connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::updateDisplayUnit); updateDisplayUnit(); // Coin Control connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &SendCoinsDialog::coinControlUpdateLabels); connect(_model->getOptionsModel(), &OptionsModel::coinControlFeaturesChanged, this, &SendCoinsDialog::coinControlFeatureChanged); ui->frameCoinControl->setVisible( _model->getOptionsModel()->getCoinControlFeatures()); coinControlUpdateLabels(); // fee section #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) const auto buttonClickedEvent = QOverload::of(&QButtonGroup::idClicked); #else /* QOverload in introduced from Qt 5.7, but we support down to 5.5.1 */ const auto buttonClickedEvent = static_cast( &QButtonGroup::buttonClicked); #endif connect(ui->groupFee, buttonClickedEvent, this, &SendCoinsDialog::updateFeeSectionControls); connect(ui->groupFee, buttonClickedEvent, this, &SendCoinsDialog::coinControlUpdateLabels); connect(ui->customFee, &BitcoinAmountField::valueChanged, this, &SendCoinsDialog::coinControlUpdateLabels); - connect(ui->checkBoxMinimumFee, &QCheckBox::stateChanged, this, - &SendCoinsDialog::setMinimumFee); - connect(ui->checkBoxMinimumFee, &QCheckBox::stateChanged, this, - &SendCoinsDialog::updateFeeSectionControls); - connect(ui->checkBoxMinimumFee, &QCheckBox::stateChanged, this, - &SendCoinsDialog::coinControlUpdateLabels); - - ui->customFee->setSingleStep(model->wallet().getRequiredFee(1000)); + Amount requiredFee = model->wallet().getRequiredFee(1000); + ui->customFee->SetMinValue(requiredFee); + if (ui->customFee->value() < requiredFee) { + ui->customFee->setValue(requiredFee); + } + ui->customFee->setSingleStep(requiredFee); updateFeeSectionControls(); - updateMinFeeLabel(); updateSmartFeeLabel(); if (model->wallet().privateKeysDisabled()) { ui->sendButton->setText(tr("Cr&eate Unsigned")); ui->sendButton->setToolTip( tr("Creates a Partially Signed Bitcoin Transaction (PSBT) for " "use with e.g. an offline %1 wallet, or a PSBT-compatible " "hardware wallet.") .arg(PACKAGE_NAME)); } } } SendCoinsDialog::~SendCoinsDialog() { QSettings settings; settings.setValue("fFeeSectionMinimized", fFeeMinimized); settings.setValue("nFeeRadio", ui->groupFee->checkedId()); settings.setValue("nTransactionFee", qint64(ui->customFee->value() / SATOSHI)); - settings.setValue("fPayOnlyMinFee", ui->checkBoxMinimumFee->isChecked()); delete ui; } bool SendCoinsDialog::PrepareSendText(QString &question_string, QString &informative_text, QString &detailed_text) { QList recipients; bool valid = true; for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if (entry) { if (entry->validate(model->node())) { recipients.append(entry->getValue()); } else if (valid) { ui->scrollArea->ensureWidgetVisible(entry); valid = false; } } } if (!valid || recipients.isEmpty()) { return false; } fNewRecipientAllowed = false; WalletModel::UnlockContext ctx(model->requestUnlock()); if (!ctx.isValid()) { // Unlock wallet was cancelled fNewRecipientAllowed = true; return false; } // prepare transaction for getting txFee earlier m_current_transaction = std::make_unique(recipients); WalletModel::SendCoinsReturn prepareStatus; // Always use a CCoinControl instance, use the CoinControlDialog instance if // CoinControl has been enabled CCoinControl ctrl; if (model->getOptionsModel()->getCoinControlFeatures()) { ctrl = *CoinControlDialog::coinControl(); } updateCoinControlState(ctrl); prepareStatus = model->prepareTransaction(*m_current_transaction, ctrl); // process prepareStatus and on error generate message shown to user processSendCoinsReturn(prepareStatus, BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), m_current_transaction->getTransactionFee())); if (prepareStatus.status != WalletModel::OK) { fNewRecipientAllowed = true; return false; } Amount txFee = m_current_transaction->getTransactionFee(); QStringList formatted; for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients()) { // generate amount string with wallet name in case of multiwallet QString amount = BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), rcp.amount); if (model->isMultiwallet()) { amount.append( tr(" from wallet '%1'") .arg(GUIUtil::HtmlEscape(model->getWalletName()))); } // generate address string QString address = rcp.address; QString recipientElement; #ifdef ENABLE_BIP70 // normal payment if (!rcp.paymentRequest.IsInitialized()) #endif { if (rcp.label.length() > 0) { // label with address recipientElement.append( tr("%1 to '%2'") .arg(amount, GUIUtil::HtmlEscape(rcp.label))); recipientElement.append(QString(" (%1)").arg(address)); } else { // just address recipientElement.append(tr("%1 to %2").arg(amount, address)); } } #ifdef ENABLE_BIP70 // authenticated payment request else if (!rcp.authenticatedMerchant.isEmpty()) { recipientElement.append( tr("%1 to '%2'").arg(amount, rcp.authenticatedMerchant)); } else { // unauthenticated payment request recipientElement.append(tr("%1 to %2").arg(amount, address)); } #endif formatted.append(recipientElement); } if (model->wallet().privateKeysDisabled()) { question_string.append(tr("Do you want to draft this transaction?")); } else { question_string.append(tr("Are you sure you want to send?")); } question_string.append("
"); if (model->wallet().privateKeysDisabled()) { question_string.append( tr("Please, review your transaction proposal. This will produce a " "Partially Signed Bitcoin 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(PACKAGE_NAME)); } else { question_string.append(tr("Please, review your transaction.")); } question_string.append("%1"); if (txFee > Amount::zero()) { // append fee string if a fee is required question_string.append("
"); question_string.append(tr("Transaction fee")); question_string.append(""); // append transaction size question_string.append( " (" + QString::number( (double)m_current_transaction->getTransactionSize() / 1000) + " kB): "); // append transaction fee value question_string.append( ""); question_string.append(BitcoinUnits::formatHtmlWithUnit( model->getOptionsModel()->getDisplayUnit(), txFee)); question_string.append("
"); } // add total amount in all subdivision units question_string.append("
"); Amount totalAmount = m_current_transaction->getTotalTransactionAmount() + txFee; QStringList alternativeUnits; for (const BitcoinUnits::Unit u : BitcoinUnits::availableUnits()) { if (u != model->getOptionsModel()->getDisplayUnit()) { alternativeUnits.append( BitcoinUnits::formatHtmlWithUnit(u, totalAmount)); } } question_string.append( QString("%1: %2") .arg(tr("Total Amount")) .arg(BitcoinUnits::formatHtmlWithUnit( model->getOptionsModel()->getDisplayUnit(), totalAmount))); question_string.append( QString("
(=%1)") .arg(alternativeUnits.join(" " + tr("or") + " "))); if (formatted.size() > 1) { question_string = question_string.arg(""); informative_text = tr("To review recipient list click \"Show Details...\""); detailed_text = formatted.join("\n\n"); } else { question_string = question_string.arg("

" + formatted.at(0)); } return true; } void SendCoinsDialog::on_sendButton_clicked() { if (!model || !model->getOptionsModel()) { return; } QString question_string, informative_text, detailed_text; if (!PrepareSendText(question_string, informative_text, detailed_text)) { return; } assert(m_current_transaction); const QString confirmation = model->wallet().privateKeysDisabled() ? tr("Confirm transaction proposal") : tr("Confirm send coins"); const QString confirmButtonText = model->wallet().privateKeysDisabled() ? tr("Create Unsigned") : tr("Send"); SendConfirmationDialog confirmationDialog( confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this); confirmationDialog.exec(); QMessageBox::StandardButton retval = static_cast(confirmationDialog.result()); if (retval != QMessageBox::Yes) { fNewRecipientAllowed = true; return; } bool send_failure = false; if (model->wallet().privateKeysDisabled()) { CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())}; PartiallySignedTransaction psbtx(mtx); bool complete = false; const TransactionError err = model->wallet().fillPSBT( SigHashType().withForkId(), false /* sign */, true /* bip32derivs */, psbtx, complete); assert(!complete); assert(err == TransactionError::OK); // Serialize the PSBT CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); ssTx << psbtx; GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str()); QMessageBox msgBox; msgBox.setText("Unsigned Transaction"); msgBox.setInformativeText( "The PSBT has been copied to the clipboard. You can also save it."); msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard); msgBox.setDefaultButton(QMessageBox::Discard); switch (msgBox.exec()) { case QMessageBox::Save: { QString selectedFilter; QString fileNameSuggestion = ""; bool first = true; for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients()) { if (!first) { fileNameSuggestion.append(" - "); } QString labelOrAddress = rcp.label.isEmpty() ? rcp.address : rcp.label; QString amount = BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), rcp.amount); fileNameSuggestion.append(labelOrAddress + "-" + amount); first = false; } fileNameSuggestion.append(".psbt"); QString filename = GUIUtil::getSaveFileName( this, tr("Save Transaction Data"), fileNameSuggestion, tr("Partially Signed Transaction (Binary) (*.psbt)"), &selectedFilter); if (filename.isEmpty()) { return; } std::ofstream out(filename.toLocal8Bit().data()); out << ssTx.str(); out.close(); Q_EMIT message(tr("PSBT saved"), "PSBT saved to disk", CClientUIInterface::MSG_INFORMATION); break; } case QMessageBox::Discard: break; default: assert(false); } } else { // now send the prepared transaction WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction); // process sendStatus and on error generate message shown to user processSendCoinsReturn(sendStatus); if (sendStatus.status == WalletModel::OK) { Q_EMIT coinsSent(m_current_transaction->getWtx()->GetId()); } else { send_failure = true; } } if (!send_failure) { accept(); CoinControlDialog::coinControl()->UnSelectAll(); coinControlUpdateLabels(); } fNewRecipientAllowed = true; m_current_transaction.reset(); } void SendCoinsDialog::clear() { m_current_transaction.reset(); // Clear coin control settings CoinControlDialog::coinControl()->UnSelectAll(); ui->checkBoxCoinControlChange->setChecked(false); ui->lineEditCoinControlChange->clear(); coinControlUpdateLabels(); // Remove entries until only one left while (ui->entries->count()) { ui->entries->takeAt(0)->widget()->deleteLater(); } addEntry(); updateTabsAndLabels(); } void SendCoinsDialog::reject() { clear(); } void SendCoinsDialog::accept() { clear(); } SendCoinsEntry *SendCoinsDialog::addEntry() { SendCoinsEntry *entry = new SendCoinsEntry(platformStyle, model, this); ui->entries->addWidget(entry); connect(entry, &SendCoinsEntry::removeEntry, this, &SendCoinsDialog::removeEntry); connect(entry, &SendCoinsEntry::useAvailableBalance, this, &SendCoinsDialog::useAvailableBalance); connect(entry, &SendCoinsEntry::payAmountChanged, this, &SendCoinsDialog::coinControlUpdateLabels); connect(entry, &SendCoinsEntry::subtractFeeFromAmountChanged, this, &SendCoinsDialog::coinControlUpdateLabels); // Focus the field, so that entry can start immediately entry->clear(); entry->setFocus(); ui->scrollAreaWidgetContents->resize( ui->scrollAreaWidgetContents->sizeHint()); qApp->processEvents(); QScrollBar *bar = ui->scrollArea->verticalScrollBar(); if (bar) { bar->setSliderPosition(bar->maximum()); } updateTabsAndLabels(); return entry; } void SendCoinsDialog::updateTabsAndLabels() { setupTabChain(nullptr); coinControlUpdateLabels(); } void SendCoinsDialog::removeEntry(SendCoinsEntry *entry) { entry->hide(); // If the last entry is about to be removed add an empty one if (ui->entries->count() == 1) { addEntry(); } entry->deleteLater(); updateTabsAndLabels(); } QWidget *SendCoinsDialog::setupTabChain(QWidget *prev) { for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if (entry) { prev = entry->setupTabChain(prev); } } QWidget::setTabOrder(prev, ui->sendButton); QWidget::setTabOrder(ui->sendButton, ui->clearButton); QWidget::setTabOrder(ui->clearButton, ui->addButton); return ui->addButton; } void SendCoinsDialog::setAddress(const QString &address) { SendCoinsEntry *entry = nullptr; // Replace the first entry if it is still unused if (ui->entries->count() == 1) { SendCoinsEntry *first = qobject_cast(ui->entries->itemAt(0)->widget()); if (first->isClear()) { entry = first; } } if (!entry) { entry = addEntry(); } entry->setAddress(address); } void SendCoinsDialog::pasteEntry(const SendCoinsRecipient &rv) { if (!fNewRecipientAllowed) { return; } SendCoinsEntry *entry = nullptr; // Replace the first entry if it is still unused if (ui->entries->count() == 1) { SendCoinsEntry *first = qobject_cast(ui->entries->itemAt(0)->widget()); if (first->isClear()) { entry = first; } } if (!entry) { entry = addEntry(); } entry->setValue(rv); updateTabsAndLabels(); } bool SendCoinsDialog::handlePaymentRequest(const SendCoinsRecipient &rv) { // Just paste the entry, all pre-checks are done in paymentserver.cpp. pasteEntry(rv); return true; } void SendCoinsDialog::setBalance(const interfaces::WalletBalances &balances) { if (model && model->getOptionsModel()) { Amount balance = balances.balance; if (model->wallet().privateKeysDisabled()) { balance = balances.watch_only_balance; ui->labelBalanceName->setText(tr("Watch-only balance:")); } ui->labelBalance->setText(BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), balance)); } } void SendCoinsDialog::updateDisplayUnit() { setBalance(model->wallet().getBalances()); ui->customFee->setDisplayUnit(model->getOptionsModel()->getDisplayUnit()); - updateMinFeeLabel(); updateSmartFeeLabel(); } void SendCoinsDialog::processSendCoinsReturn( const WalletModel::SendCoinsReturn &sendCoinsReturn, const QString &msgArg) { QPair msgParams; // Default to a warning message, override if error message is needed msgParams.second = CClientUIInterface::MSG_WARNING; // This comment is specific to SendCoinsDialog usage of // WalletModel::SendCoinsReturn. // All status values are used only in WalletModel::prepareTransaction() switch (sendCoinsReturn.status) { case WalletModel::InvalidAddress: msgParams.first = tr("The recipient address is not valid. Please recheck."); break; case WalletModel::InvalidAmount: msgParams.first = tr("The amount to pay must be larger than 0."); break; case WalletModel::AmountExceedsBalance: msgParams.first = tr("The amount exceeds your balance."); break; case WalletModel::AmountWithFeeExceedsBalance: msgParams.first = tr("The total exceeds your balance when the %1 " "transaction fee is included.") .arg(msgArg); break; case WalletModel::DuplicateAddress: msgParams.first = tr("Duplicate address found: addresses should " "only be used once each."); break; case WalletModel::TransactionCreationFailed: msgParams.first = tr("Transaction creation failed!"); msgParams.second = CClientUIInterface::MSG_ERROR; break; case WalletModel::AbsurdFee: msgParams.first = tr("A fee higher than %1 is considered an absurdly high fee.") .arg(BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), model->wallet().getDefaultMaxTxFee())); break; case WalletModel::PaymentRequestExpired: msgParams.first = tr("Payment request expired."); msgParams.second = CClientUIInterface::MSG_ERROR; break; // included to prevent a compiler warning. case WalletModel::OK: default: return; } Q_EMIT message(tr("Send Coins"), msgParams.first, msgParams.second); } void SendCoinsDialog::minimizeFeeSection(bool fMinimize) { ui->labelFeeMinimized->setVisible(fMinimize); ui->buttonChooseFee->setVisible(fMinimize); ui->buttonMinimizeFee->setVisible(!fMinimize); ui->frameFeeSelection->setVisible(!fMinimize); ui->horizontalLayoutSmartFee->setContentsMargins(0, (fMinimize ? 0 : 6), 0, 0); fFeeMinimized = fMinimize; } void SendCoinsDialog::on_buttonChooseFee_clicked() { minimizeFeeSection(false); } void SendCoinsDialog::on_buttonMinimizeFee_clicked() { updateFeeMinimizedLabel(); minimizeFeeSection(true); } void SendCoinsDialog::useAvailableBalance(SendCoinsEntry *entry) { // Get CCoinControl instance if CoinControl is enabled or create a new one. CCoinControl coin_control; if (model->getOptionsModel()->getCoinControlFeatures()) { coin_control = *CoinControlDialog::coinControl(); } // Include watch-only for wallets without private key coin_control.fAllowWatchOnly = model->wallet().privateKeysDisabled(); // Calculate available amount to send. Amount amount = model->wallet().getAvailableBalance(coin_control); for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *e = qobject_cast(ui->entries->itemAt(i)->widget()); if (e && !e->isHidden() && e != entry) { amount -= e->getValue().amount; } } if (amount > Amount::zero()) { entry->checkSubtractFeeFromAmount(); entry->setAmount(amount); } else { entry->setAmount(Amount::zero()); } } -void SendCoinsDialog::setMinimumFee() { - ui->customFee->setValue(model->wallet().getRequiredFee(1000)); -} - void SendCoinsDialog::updateFeeSectionControls() { ui->labelSmartFee->setEnabled(ui->radioSmartFee->isChecked()); ui->labelSmartFee2->setEnabled(ui->radioSmartFee->isChecked()); ui->labelFeeEstimation->setEnabled(ui->radioSmartFee->isChecked()); - ui->checkBoxMinimumFee->setEnabled(ui->radioCustomFee->isChecked()); - ui->labelMinFeeWarning->setEnabled(ui->radioCustomFee->isChecked()); - ui->labelCustomPerKilobyte->setEnabled( - ui->radioCustomFee->isChecked() && - !ui->checkBoxMinimumFee->isChecked()); - ui->customFee->setEnabled(ui->radioCustomFee->isChecked() && - !ui->checkBoxMinimumFee->isChecked()); + ui->labelCustomFeeWarning->setEnabled(ui->radioCustomFee->isChecked()); + ui->labelCustomPerKilobyte->setEnabled(ui->radioCustomFee->isChecked()); + ui->customFee->setEnabled(ui->radioCustomFee->isChecked()); } void SendCoinsDialog::updateFeeMinimizedLabel() { if (!model || !model->getOptionsModel()) { return; } if (ui->radioSmartFee->isChecked()) { ui->labelFeeMinimized->setText(ui->labelSmartFee->text()); } else { ui->labelFeeMinimized->setText( BitcoinUnits::formatWithUnit( model->getOptionsModel()->getDisplayUnit(), ui->customFee->value()) + "/kB"); } } -void SendCoinsDialog::updateMinFeeLabel() { - if (model && model->getOptionsModel()) { - ui->checkBoxMinimumFee->setText( - tr("Pay only the required fee of %1") - .arg(BitcoinUnits::formatWithUnit( - model->getOptionsModel()->getDisplayUnit(), - model->wallet().getRequiredFee(1000)) + - "/kB")); - } -} - void SendCoinsDialog::updateCoinControlState(CCoinControl &ctrl) { if (ui->radioCustomFee->isChecked()) { ctrl.m_feerate = CFeeRate(ui->customFee->value()); } else { ctrl.m_feerate.reset(); } // Include watch-only for wallets without private key ctrl.fAllowWatchOnly = model->wallet().privateKeysDisabled(); } void SendCoinsDialog::updateSmartFeeLabel() { if (!model || !model->getOptionsModel()) { return; } CCoinControl coin_control; updateCoinControlState(coin_control); // Explicitly use only fee estimation rate for smart fee labels coin_control.m_feerate.reset(); CFeeRate feeRate(model->wallet().getMinimumFee(1000, coin_control)); ui->labelSmartFee->setText( BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), feeRate.GetFeePerK()) + "/kB"); // not enough data => minfee if (feeRate <= CFeeRate(Amount::zero())) { // (Smart fee not initialized yet. This usually takes a few blocks...) ui->labelSmartFee2->show(); ui->labelFeeEstimation->setText(""); } else { ui->labelSmartFee2->hide(); ui->labelFeeEstimation->setText( tr("Estimated to begin confirmation by next block.")); } updateFeeMinimizedLabel(); } // Coin Control: copy label "Quantity" to clipboard void SendCoinsDialog::coinControlClipboardQuantity() { GUIUtil::setClipboard(ui->labelCoinControlQuantity->text()); } // Coin Control: copy label "Amount" to clipboard void SendCoinsDialog::coinControlClipboardAmount() { GUIUtil::setClipboard(ui->labelCoinControlAmount->text().left( ui->labelCoinControlAmount->text().indexOf(" "))); } // Coin Control: copy label "Fee" to clipboard void SendCoinsDialog::coinControlClipboardFee() { GUIUtil::setClipboard( ui->labelCoinControlFee->text() .left(ui->labelCoinControlFee->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // Coin Control: copy label "After fee" to clipboard void SendCoinsDialog::coinControlClipboardAfterFee() { GUIUtil::setClipboard( ui->labelCoinControlAfterFee->text() .left(ui->labelCoinControlAfterFee->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // Coin Control: copy label "Bytes" to clipboard void SendCoinsDialog::coinControlClipboardBytes() { GUIUtil::setClipboard( ui->labelCoinControlBytes->text().replace(ASYMP_UTF8, "")); } // Coin Control: copy label "Dust" to clipboard void SendCoinsDialog::coinControlClipboardLowOutput() { GUIUtil::setClipboard(ui->labelCoinControlLowOutput->text()); } // Coin Control: copy label "Change" to clipboard void SendCoinsDialog::coinControlClipboardChange() { GUIUtil::setClipboard( ui->labelCoinControlChange->text() .left(ui->labelCoinControlChange->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // Coin Control: settings menu - coin control enabled/disabled by user void SendCoinsDialog::coinControlFeatureChanged(bool checked) { ui->frameCoinControl->setVisible(checked); // coin control features disabled if (!checked && model) { CoinControlDialog::coinControl()->SetNull(); } coinControlUpdateLabels(); } // Coin Control: button inputs -> show actual coin control dialog void SendCoinsDialog::coinControlButtonClicked() { CoinControlDialog dlg(platformStyle); dlg.setModel(model); dlg.exec(); coinControlUpdateLabels(); } // Coin Control: checkbox custom change address void SendCoinsDialog::coinControlChangeChecked(int state) { if (state == Qt::Unchecked) { CoinControlDialog::coinControl()->destChange = CNoDestination(); ui->labelCoinControlChangeLabel->clear(); } else { // use this to re-validate an already entered address coinControlChangeEdited(ui->lineEditCoinControlChange->text()); } ui->lineEditCoinControlChange->setEnabled((state == Qt::Checked)); } // Coin Control: custom change address changed void SendCoinsDialog::coinControlChangeEdited(const QString &text) { if (model && model->getAddressTableModel()) { // Default to no change address until verified CoinControlDialog::coinControl()->destChange = CNoDestination(); ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:red;}"); const CTxDestination dest = DecodeDestination(text.toStdString(), model->getChainParams()); if (text.isEmpty()) { // Nothing entered ui->labelCoinControlChangeLabel->setText(""); } else if (!IsValidDestination(dest)) { // Invalid address ui->labelCoinControlChangeLabel->setText( tr("Warning: Invalid Bitcoin address")); } else { // Valid address if (!model->wallet().isSpendable(dest)) { ui->labelCoinControlChangeLabel->setText( tr("Warning: Unknown change address")); // confirmation dialog 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?"), QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel); if (btnRetVal == QMessageBox::Yes) { CoinControlDialog::coinControl()->destChange = dest; } else { ui->lineEditCoinControlChange->setText(""); ui->labelCoinControlChangeLabel->setStyleSheet( "QLabel{color:black;}"); ui->labelCoinControlChangeLabel->setText(""); } } else { // Known change address ui->labelCoinControlChangeLabel->setStyleSheet( "QLabel{color:black;}"); // Query label QString associatedLabel = model->getAddressTableModel()->labelForAddress(text); if (!associatedLabel.isEmpty()) { ui->labelCoinControlChangeLabel->setText(associatedLabel); } else { ui->labelCoinControlChangeLabel->setText(tr("(no label)")); } CoinControlDialog::coinControl()->destChange = dest; } } } } // Coin Control: update labels void SendCoinsDialog::coinControlUpdateLabels() { if (!model || !model->getOptionsModel()) { return; } updateCoinControlState(*CoinControlDialog::coinControl()); // set pay amounts CoinControlDialog::payAmounts.clear(); CoinControlDialog::fSubtractFeeFromAmount = false; for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); if (entry && !entry->isHidden()) { SendCoinsRecipient rcp = entry->getValue(); CoinControlDialog::payAmounts.append(rcp.amount); if (rcp.fSubtractFeeFromAmount) { CoinControlDialog::fSubtractFeeFromAmount = true; } } } if (CoinControlDialog::coinControl()->HasSelected()) { // actual coin control calculation CoinControlDialog::updateLabels(model, this); // show coin control stats ui->labelCoinControlAutomaticallySelected->hide(); ui->widgetCoinControl->show(); } else { // hide coin control stats ui->labelCoinControlAutomaticallySelected->show(); ui->widgetCoinControl->hide(); ui->labelCoinControlInsuffFunds->hide(); } } SendConfirmationDialog::SendConfirmationDialog( const QString &title, const QString &text, const QString &informative_text, const QString &detailed_text, int _secDelay, const QString &_confirmButtonText, QWidget *parent) : QMessageBox(parent), secDelay(_secDelay), confirmButtonText(_confirmButtonText) { setIcon(QMessageBox::Question); // On macOS, the window title is ignored (as required by the macOS // Guidelines). setWindowTitle(title); setText(text); setInformativeText(informative_text); setDetailedText(detailed_text); setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); setDefaultButton(QMessageBox::Cancel); yesButton = button(QMessageBox::Yes); updateYesButton(); connect(&countDownTimer, &QTimer::timeout, this, &SendConfirmationDialog::countDown); } int SendConfirmationDialog::exec() { updateYesButton(); countDownTimer.start(1000); return QMessageBox::exec(); } void SendConfirmationDialog::countDown() { secDelay--; updateYesButton(); if (secDelay <= 0) { countDownTimer.stop(); } } void SendConfirmationDialog::updateYesButton() { if (secDelay > 0) { yesButton->setEnabled(false); yesButton->setText(confirmButtonText + " (" + QString::number(secDelay) + ")"); } else { yesButton->setEnabled(true); yesButton->setText(confirmButtonText); } } diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index 65dbf397b..9856ade73 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -1,140 +1,138 @@ // Copyright (c) 2011-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. #ifndef BITCOIN_QT_SENDCOINSDIALOG_H #define BITCOIN_QT_SENDCOINSDIALOG_H #include #include #include #include #include class ClientModel; class PlatformStyle; class SendCoinsEntry; class SendCoinsRecipient; namespace Ui { class SendCoinsDialog; } QT_BEGIN_NAMESPACE class QUrl; QT_END_NAMESPACE /** Dialog for sending bitcoins */ class SendCoinsDialog : public QDialog { Q_OBJECT public: SendCoinsDialog(const PlatformStyle *platformStyle, WalletModel *model, QWidget *parent = nullptr); ~SendCoinsDialog(); void setClientModel(ClientModel *clientModel); void setModel(WalletModel *model); /** * Set up the tab chain manually, as Qt messes up the tab chain by default * in some cases (issue * https://bugreports.qt-project.org/browse/QTBUG-10907). */ QWidget *setupTabChain(QWidget *prev); void setAddress(const QString &address); void pasteEntry(const SendCoinsRecipient &rv); bool handlePaymentRequest(const SendCoinsRecipient &recipient); public Q_SLOTS: void clear(); void reject() override; void accept() override; SendCoinsEntry *addEntry(); void updateTabsAndLabels(); void setBalance(const interfaces::WalletBalances &balances); Q_SIGNALS: void coinsSent(const uint256 &txid); private: Ui::SendCoinsDialog *ui; ClientModel *clientModel; WalletModel *model; std::unique_ptr m_current_transaction; bool fNewRecipientAllowed; bool fFeeMinimized; const PlatformStyle *platformStyle; // Process WalletModel::SendCoinsReturn and generate a pair consisting of a // message and message flags for use in Q_EMIT message(). // Additional parameter msgArg can be used via .arg(msgArg). void processSendCoinsReturn(const WalletModel::SendCoinsReturn &sendCoinsReturn, const QString &msgArg = QString()); void minimizeFeeSection(bool fMinimize); // Format confirmation message bool PrepareSendText(QString &question_string, QString &informative_text, QString &detailed_text); void updateFeeMinimizedLabel(); // Update the passed in CCoinControl with state from the GUI void updateCoinControlState(CCoinControl &ctrl); private Q_SLOTS: void on_sendButton_clicked(); void on_buttonChooseFee_clicked(); void on_buttonMinimizeFee_clicked(); void removeEntry(SendCoinsEntry *entry); void useAvailableBalance(SendCoinsEntry *entry); void updateDisplayUnit(); void coinControlFeatureChanged(bool); void coinControlButtonClicked(); void coinControlChangeChecked(int); void coinControlChangeEdited(const QString &); void coinControlUpdateLabels(); void coinControlClipboardQuantity(); void coinControlClipboardAmount(); void coinControlClipboardFee(); void coinControlClipboardAfterFee(); void coinControlClipboardBytes(); void coinControlClipboardLowOutput(); void coinControlClipboardChange(); - void setMinimumFee(); void updateFeeSectionControls(); - void updateMinFeeLabel(); void updateSmartFeeLabel(); Q_SIGNALS: // Fired when a message should be reported to the user void message(const QString &title, const QString &message, unsigned int style); }; #define SEND_CONFIRM_DELAY 3 class SendConfirmationDialog : public QMessageBox { Q_OBJECT public: SendConfirmationDialog(const QString &title, const QString &text, const QString &informative_text = "", const QString &detailed_text = "", int secDelay = SEND_CONFIRM_DELAY, const QString &confirmText = "Send", QWidget *parent = nullptr); int exec(); private Q_SLOTS: void countDown(); void updateYesButton(); private: QAbstractButton *yesButton; QTimer countDownTimer; int secDelay; QString confirmButtonText; }; #endif // BITCOIN_QT_SENDCOINSDIALOG_H