diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index c0708a7cc..240b7a7f3 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -1,821 +1,816 @@ // 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 #include QList CoinControlDialog::payAmounts; bool CoinControlDialog::fSubtractFeeFromAmount = false; bool CCoinControlWidgetItem::operator<(const QTreeWidgetItem &other) const { int column = treeWidget()->sortColumn(); if (column == CoinControlDialog::COLUMN_AMOUNT || column == CoinControlDialog::COLUMN_DATE || column == CoinControlDialog::COLUMN_CONFIRMATIONS) { return data(column, Qt::UserRole).toLongLong() < other.data(column, Qt::UserRole).toLongLong(); } return QTreeWidgetItem::operator<(other); } -CoinControlDialog::CoinControlDialog(const PlatformStyle *_platformStyle, +CoinControlDialog::CoinControlDialog(CCoinControl &coin_control, + WalletModel *_model, + const PlatformStyle *_platformStyle, QWidget *parent) - : QDialog(parent), ui(new Ui::CoinControlDialog), model(nullptr), + : QDialog(parent), ui(new Ui::CoinControlDialog), + m_coin_control(coin_control), model(_model), platformStyle(_platformStyle) { ui->setupUi(this); // context menu actions QAction *copyAddressAction = new QAction(tr("Copy address"), this); QAction *copyLabelAction = new QAction(tr("Copy label"), this); QAction *copyAmountAction = new QAction(tr("Copy amount"), this); // we need to enable/disable this copyTransactionHashAction = new QAction(tr("Copy transaction ID"), this); // we need to enable/disable this lockAction = new QAction(tr("Lock unspent"), this); // we need to enable/disable this unlockAction = new QAction(tr("Unlock unspent"), this); // context menu contextMenu = new QMenu(this); contextMenu->addAction(copyAddressAction); contextMenu->addAction(copyLabelAction); contextMenu->addAction(copyAmountAction); contextMenu->addAction(copyTransactionHashAction); contextMenu->addSeparator(); contextMenu->addAction(lockAction); contextMenu->addAction(unlockAction); // context menu signals connect(ui->treeWidget, &QWidget::customContextMenuRequested, this, &CoinControlDialog::showMenu); connect(copyAddressAction, &QAction::triggered, this, &CoinControlDialog::copyAddress); connect(copyLabelAction, &QAction::triggered, this, &CoinControlDialog::copyLabel); connect(copyAmountAction, &QAction::triggered, this, &CoinControlDialog::copyAmount); connect(copyTransactionHashAction, &QAction::triggered, this, &CoinControlDialog::copyTransactionHash); connect(lockAction, &QAction::triggered, this, &CoinControlDialog::lockCoin); connect(unlockAction, &QAction::triggered, this, &CoinControlDialog::unlockCoin); // 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, &CoinControlDialog::clipboardQuantity); connect(clipboardAmountAction, &QAction::triggered, this, &CoinControlDialog::clipboardAmount); connect(clipboardFeeAction, &QAction::triggered, this, &CoinControlDialog::clipboardFee); connect(clipboardAfterFeeAction, &QAction::triggered, this, &CoinControlDialog::clipboardAfterFee); connect(clipboardBytesAction, &QAction::triggered, this, &CoinControlDialog::clipboardBytes); connect(clipboardLowOutputAction, &QAction::triggered, this, &CoinControlDialog::clipboardLowOutput); connect(clipboardChangeAction, &QAction::triggered, this, &CoinControlDialog::clipboardChange); 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); // toggle tree/list mode connect(ui->radioTreeMode, &QRadioButton::toggled, this, &CoinControlDialog::radioTreeMode); connect(ui->radioListMode, &QRadioButton::toggled, this, &CoinControlDialog::radioListMode); // click on checkbox connect(ui->treeWidget, &QTreeWidget::itemChanged, this, &CoinControlDialog::viewItemChanged); // click on header ui->treeWidget->header()->setSectionsClickable(true); connect(ui->treeWidget->header(), &QHeaderView::sectionClicked, this, &CoinControlDialog::headerSectionClicked); // ok button connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &CoinControlDialog::buttonBoxClicked); // (un)select all connect(ui->pushButtonSelectAll, &QPushButton::clicked, this, &CoinControlDialog::buttonSelectAllClicked); ui->treeWidget->setColumnWidth(COLUMN_CHECKBOX, 84); ui->treeWidget->setColumnWidth(COLUMN_AMOUNT, 110); ui->treeWidget->setColumnWidth(COLUMN_LABEL, 190); ui->treeWidget->setColumnWidth(COLUMN_ADDRESS, 320); ui->treeWidget->setColumnWidth(COLUMN_DATE, 130); ui->treeWidget->setColumnWidth(COLUMN_CONFIRMATIONS, 110); // default view is sorted by amount desc sortView(COLUMN_AMOUNT, Qt::DescendingOrder); // restore list mode and sortorder as a convenience feature QSettings settings; if (settings.contains("nCoinControlMode") && !settings.value("nCoinControlMode").toBool()) { ui->radioTreeMode->click(); } if (settings.contains("nCoinControlSortColumn") && settings.contains("nCoinControlSortOrder")) { sortView(settings.value("nCoinControlSortColumn").toInt(), (static_cast( settings.value("nCoinControlSortOrder").toInt()))); } GUIUtil::handleCloseWindowShortcut(this); + + if (_model->getOptionsModel() && _model->getAddressTableModel()) { + updateView(); + updateLabelLocked(); + CoinControlDialog::updateLabels(m_coin_control, _model, this); + } } CoinControlDialog::~CoinControlDialog() { QSettings settings; settings.setValue("nCoinControlMode", ui->radioListMode->isChecked()); settings.setValue("nCoinControlSortColumn", sortColumn); settings.setValue("nCoinControlSortOrder", (int)sortOrder); delete ui; } -void CoinControlDialog::setModel(WalletModel *_model) { - this->model = _model; - - if (_model && _model->getOptionsModel() && _model->getAddressTableModel()) { - updateView(); - updateLabelLocked(); - CoinControlDialog::updateLabels(_model, this); - } -} - // ok button void CoinControlDialog::buttonBoxClicked(QAbstractButton *button) { if (ui->buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { // closes the dialog done(QDialog::Accepted); } } // (un)select all void CoinControlDialog::buttonSelectAllClicked() { Qt::CheckState state = Qt::Checked; for (int i = 0; i < ui->treeWidget->topLevelItemCount(); i++) { if (ui->treeWidget->topLevelItem(i)->checkState(COLUMN_CHECKBOX) != Qt::Unchecked) { state = Qt::Unchecked; break; } } ui->treeWidget->setEnabled(false); for (int i = 0; i < ui->treeWidget->topLevelItemCount(); i++) { if (ui->treeWidget->topLevelItem(i)->checkState(COLUMN_CHECKBOX) != state) { ui->treeWidget->topLevelItem(i)->setCheckState(COLUMN_CHECKBOX, state); } } ui->treeWidget->setEnabled(true); if (state == Qt::Unchecked) { // just to be sure - coinControl()->UnSelectAll(); + m_coin_control.UnSelectAll(); } - CoinControlDialog::updateLabels(model, this); + CoinControlDialog::updateLabels(m_coin_control, model, this); } // context menu void CoinControlDialog::showMenu(const QPoint &point) { QTreeWidgetItem *item = ui->treeWidget->itemAt(point); if (item) { contextMenuItem = item; // disable some items (like Copy Transaction ID, lock, unlock) for tree // roots in context menu if (item->data(COLUMN_ADDRESS, TxIdRole).toString().length() == 64) { COutPoint outpoint = buildOutPoint(item); // transaction hash is 64 characters (this means it is a child node, // so it is not a parent node in tree mode) copyTransactionHashAction->setEnabled(true); if (model->wallet().isLockedCoin(outpoint)) { lockAction->setEnabled(false); unlockAction->setEnabled(true); } else { lockAction->setEnabled(true); unlockAction->setEnabled(false); } } else { // this means click on parent node in tree mode -> disable all copyTransactionHashAction->setEnabled(false); lockAction->setEnabled(false); unlockAction->setEnabled(false); } // show context menu contextMenu->exec(QCursor::pos()); } } // context menu action: copy amount void CoinControlDialog::copyAmount() { GUIUtil::setClipboard( BitcoinUnits::removeSpaces(contextMenuItem->text(COLUMN_AMOUNT))); } // context menu action: copy label void CoinControlDialog::copyLabel() { if (ui->radioTreeMode->isChecked() && contextMenuItem->text(COLUMN_LABEL).length() == 0 && contextMenuItem->parent()) { GUIUtil::setClipboard(contextMenuItem->parent()->text(COLUMN_LABEL)); } else { GUIUtil::setClipboard(contextMenuItem->text(COLUMN_LABEL)); } } // context menu action: copy address void CoinControlDialog::copyAddress() { if (ui->radioTreeMode->isChecked() && contextMenuItem->text(COLUMN_ADDRESS).length() == 0 && contextMenuItem->parent()) { GUIUtil::setClipboard(contextMenuItem->parent()->text(COLUMN_ADDRESS)); } else { GUIUtil::setClipboard(contextMenuItem->text(COLUMN_ADDRESS)); } } // context menu action: copy transaction id void CoinControlDialog::copyTransactionHash() { GUIUtil::setClipboard( contextMenuItem->data(COLUMN_ADDRESS, TxIdRole).toString()); } // context menu action: lock coin void CoinControlDialog::lockCoin() { if (contextMenuItem->checkState(COLUMN_CHECKBOX) == Qt::Checked) { contextMenuItem->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); } COutPoint outpoint = buildOutPoint(contextMenuItem); model->wallet().lockCoin(outpoint); contextMenuItem->setDisabled(true); contextMenuItem->setIcon( COLUMN_CHECKBOX, platformStyle->SingleColorIcon(":/icons/lock_closed")); updateLabelLocked(); } // context menu action: unlock coin void CoinControlDialog::unlockCoin() { COutPoint outpoint = buildOutPoint(contextMenuItem); model->wallet().unlockCoin(outpoint); contextMenuItem->setDisabled(false); contextMenuItem->setIcon(COLUMN_CHECKBOX, QIcon()); updateLabelLocked(); } // copy label "Quantity" to clipboard void CoinControlDialog::clipboardQuantity() { GUIUtil::setClipboard(ui->labelCoinControlQuantity->text()); } // copy label "Amount" to clipboard void CoinControlDialog::clipboardAmount() { GUIUtil::setClipboard(ui->labelCoinControlAmount->text().left( ui->labelCoinControlAmount->text().indexOf(" "))); } // copy label "Fee" to clipboard void CoinControlDialog::clipboardFee() { GUIUtil::setClipboard( ui->labelCoinControlFee->text() .left(ui->labelCoinControlFee->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // copy label "After fee" to clipboard void CoinControlDialog::clipboardAfterFee() { GUIUtil::setClipboard( ui->labelCoinControlAfterFee->text() .left(ui->labelCoinControlAfterFee->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // copy label "Bytes" to clipboard void CoinControlDialog::clipboardBytes() { GUIUtil::setClipboard( ui->labelCoinControlBytes->text().replace(ASYMP_UTF8, "")); } // copy label "Dust" to clipboard void CoinControlDialog::clipboardLowOutput() { GUIUtil::setClipboard(ui->labelCoinControlLowOutput->text()); } // copy label "Change" to clipboard void CoinControlDialog::clipboardChange() { GUIUtil::setClipboard( ui->labelCoinControlChange->text() .left(ui->labelCoinControlChange->text().indexOf(" ")) .replace(ASYMP_UTF8, "")); } // treeview: sort void CoinControlDialog::sortView(int column, Qt::SortOrder order) { sortColumn = column; sortOrder = order; ui->treeWidget->sortItems(column, order); ui->treeWidget->header()->setSortIndicator(sortColumn, sortOrder); } // treeview: clicked on header void CoinControlDialog::headerSectionClicked(int logicalIndex) { // click on most left column -> do nothing if (logicalIndex == COLUMN_CHECKBOX) { ui->treeWidget->header()->setSortIndicator(sortColumn, sortOrder); } else { if (sortColumn == logicalIndex) { sortOrder = ((sortOrder == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder); } else { sortColumn = logicalIndex; // if label or address then default => asc, else default => desc sortOrder = ((sortColumn == COLUMN_LABEL || sortColumn == COLUMN_ADDRESS) ? Qt::AscendingOrder : Qt::DescendingOrder); } sortView(sortColumn, sortOrder); } } // toggle tree mode void CoinControlDialog::radioTreeMode(bool checked) { if (checked && model) { updateView(); } } // toggle list mode void CoinControlDialog::radioListMode(bool checked) { if (checked && model) { updateView(); } } // checkbox clicked by user void CoinControlDialog::viewItemChanged(QTreeWidgetItem *item, int column) { // transaction hash is 64 characters (this means it is a child node, so it // is not a parent node in tree mode) if (column == COLUMN_CHECKBOX && item->data(COLUMN_ADDRESS, TxIdRole).toString().length() == 64) { COutPoint outpoint = buildOutPoint(item); if (item->checkState(COLUMN_CHECKBOX) == Qt::Unchecked) { - coinControl()->UnSelect(outpoint); + m_coin_control.UnSelect(outpoint); } else if (item->isDisabled()) { // locked (this happens if "check all" through parent node) item->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); } else { - coinControl()->Select(outpoint); + m_coin_control.Select(outpoint); } // selection changed -> update labels if (ui->treeWidget->isEnabled()) { // do not update on every click for (un)select all - CoinControlDialog::updateLabels(model, this); + CoinControlDialog::updateLabels(m_coin_control, model, this); } } } // shows count of locked unspent outputs void CoinControlDialog::updateLabelLocked() { std::vector vOutpts; model->wallet().listLockedCoins(vOutpts); if (vOutpts.size() > 0) { ui->labelLocked->setText(tr("(%1 locked)").arg(vOutpts.size())); ui->labelLocked->setVisible(true); } else { ui->labelLocked->setVisible(false); } } -void CoinControlDialog::updateLabels(WalletModel *model, QDialog *dialog) { +void CoinControlDialog::updateLabels(CCoinControl &m_coin_control, + WalletModel *model, QDialog *dialog) { if (!model) { return; } // nPayAmount Amount nPayAmount = Amount::zero(); bool fDust = false; CMutableTransaction txDummy; for (const Amount &amount : CoinControlDialog::payAmounts) { nPayAmount += amount; if (amount > Amount::zero()) { // Assumes a p2pkh script size CTxOut txout(amount, CScript() << std::vector(24, 0)); txDummy.vout.push_back(txout); fDust |= IsDust(txout, model->node().getDustRelayFee()); } } Amount nAmount = Amount::zero(); Amount nPayFee = Amount::zero(); Amount nAfterFee = Amount::zero(); Amount nChange = Amount::zero(); unsigned int nBytes = 0; unsigned int nBytesInputs = 0; unsigned int nQuantity = 0; std::vector vCoinControl; - coinControl()->ListSelected(vCoinControl); + m_coin_control.ListSelected(vCoinControl); size_t i = 0; for (const auto &out : model->wallet().getCoins(vCoinControl)) { if (out.depth_in_main_chain < 0) { continue; } // unselect already spent, very unlikely scenario, this could happen // when selected are spent elsewhere, like rpc or another computer const COutPoint &output = vCoinControl[i++]; if (out.is_spent) { - coinControl()->UnSelect(output); + m_coin_control.UnSelect(output); continue; } // Quantity nQuantity++; // Amount nAmount += out.txout.nValue; // Bytes CTxDestination address; if (ExtractDestination(out.txout.scriptPubKey, address)) { CPubKey pubkey; PKHash *pkhash = boost::get(&address); if (pkhash && model->wallet().getPubKey(out.txout.scriptPubKey, CKeyID(*pkhash), pubkey)) { nBytesInputs += (pubkey.IsCompressed() ? 148 : 180); } else { // in all error cases, simply assume 148 here nBytesInputs += 148; } } else { nBytesInputs += 148; } } // calculation if (nQuantity > 0) { // Bytes // always assume +1 output for change here nBytes = nBytesInputs + ((CoinControlDialog::payAmounts.size() > 0 ? CoinControlDialog::payAmounts.size() + 1 : 2) * 34) + 10; // in the subtract fee from amount case, we can tell if zero change // already and subtract the bytes, so that fee calculation afterwards is // accurate if (CoinControlDialog::fSubtractFeeFromAmount) { if (nAmount - nPayAmount == Amount::zero()) { nBytes -= 34; } } // Fee - nPayFee = model->wallet().getMinimumFee(nBytes, *coinControl()); + nPayFee = model->wallet().getMinimumFee(nBytes, m_coin_control); if (nPayAmount > Amount::zero()) { nChange = nAmount - nPayAmount; if (!CoinControlDialog::fSubtractFeeFromAmount) { nChange -= nPayFee; } // Never create dust outputs; if we would, just add the dust to the // fee. if (nChange > Amount::zero() && nChange < MIN_CHANGE) { // Assumes a p2pkh script size CTxOut txout(nChange, CScript() << std::vector(24, 0)); if (IsDust(txout, model->node().getDustRelayFee())) { nPayFee += nChange; nChange = Amount::zero(); if (CoinControlDialog::fSubtractFeeFromAmount) { // we didn't detect lack of change above nBytes -= 34; } } } if (nChange == Amount::zero() && !CoinControlDialog::fSubtractFeeFromAmount) { nBytes -= 34; } } // after fee nAfterFee = std::max(nAmount - nPayFee, Amount::zero()); } // actually update labels int nDisplayUnit = BitcoinUnits::BCH; if (model && model->getOptionsModel()) { nDisplayUnit = model->getOptionsModel()->getDisplayUnit(); } QLabel *l1 = dialog->findChild("labelCoinControlQuantity"); QLabel *l2 = dialog->findChild("labelCoinControlAmount"); QLabel *l3 = dialog->findChild("labelCoinControlFee"); QLabel *l4 = dialog->findChild("labelCoinControlAfterFee"); QLabel *l5 = dialog->findChild("labelCoinControlBytes"); QLabel *l7 = dialog->findChild("labelCoinControlLowOutput"); QLabel *l8 = dialog->findChild("labelCoinControlChange"); // enable/disable "dust" and "change" dialog->findChild("labelCoinControlLowOutputText") ->setEnabled(nPayAmount > Amount::zero()); dialog->findChild("labelCoinControlLowOutput") ->setEnabled(nPayAmount > Amount::zero()); dialog->findChild("labelCoinControlChangeText") ->setEnabled(nPayAmount > Amount::zero()); dialog->findChild("labelCoinControlChange") ->setEnabled(nPayAmount > Amount::zero()); // stats // Quantity l1->setText(QString::number(nQuantity)); // Amount l2->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAmount)); // Fee l3->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nPayFee)); // After Fee l4->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAfterFee)); // Bytes l5->setText(((nBytes > 0) ? ASYMP_UTF8 : "") + QString::number(nBytes)); // Dust l7->setText(fDust ? tr("yes") : tr("no")); // Change l8->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nChange)); if (nPayFee > Amount::zero()) { l3->setText(ASYMP_UTF8 + l3->text()); l4->setText(ASYMP_UTF8 + l4->text()); if (nChange > Amount::zero() && !CoinControlDialog::fSubtractFeeFromAmount) { l8->setText(ASYMP_UTF8 + l8->text()); } } // turn label red when dust l7->setStyleSheet((fDust) ? "color:red;" : ""); // tool tips QString toolTipDust = tr("This label turns red if any recipient receives an amount smaller " "than the current dust threshold."); // how many satoshis the estimated fee can vary per byte we guess wrong double dFeeVary = (nBytes != 0) ? double(nPayFee / SATOSHI) / nBytes : 0; QString toolTip4 = tr("Can vary +/- %1 satoshi(s) per input.").arg(dFeeVary); l3->setToolTip(toolTip4); l4->setToolTip(toolTip4); l7->setToolTip(toolTipDust); l8->setToolTip(toolTip4); dialog->findChild("labelCoinControlFeeText") ->setToolTip(l3->toolTip()); dialog->findChild("labelCoinControlAfterFeeText") ->setToolTip(l4->toolTip()); dialog->findChild("labelCoinControlBytesText") ->setToolTip(l5->toolTip()); dialog->findChild("labelCoinControlLowOutputText") ->setToolTip(l7->toolTip()); dialog->findChild("labelCoinControlChangeText") ->setToolTip(l8->toolTip()); // Insufficient funds QLabel *label = dialog->findChild("labelCoinControlInsuffFunds"); if (label) { label->setVisible(nChange < Amount::zero()); } } -CCoinControl *CoinControlDialog::coinControl() { - static CCoinControl coin_control; - return &coin_control; -} - COutPoint CoinControlDialog::buildOutPoint(const QTreeWidgetItem *item) { TxId txid; txid.SetHex(item->data(COLUMN_ADDRESS, TxIdRole).toString().toStdString()); return COutPoint(txid, item->data(COLUMN_ADDRESS, VOutRole).toUInt()); } void CoinControlDialog::updateView() { if (!model || !model->getOptionsModel() || !model->getAddressTableModel()) { return; } bool treeMode = ui->radioTreeMode->isChecked(); ui->treeWidget->clear(); // performance, otherwise updateLabels would be called for every checked // checkbox ui->treeWidget->setEnabled(false); ui->treeWidget->setAlternatingRowColors(!treeMode); QFlags flgCheckbox = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; QFlags flgTristate = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsTristate; int nDisplayUnit = model->getOptionsModel()->getDisplayUnit(); for (const auto &coins : model->wallet().listCoins()) { CCoinControlWidgetItem *itemWalletAddress = new CCoinControlWidgetItem(); itemWalletAddress->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); QString sWalletAddress = QString::fromStdString( EncodeCashAddr(coins.first, model->getChainParams())); QString sWalletLabel = model->getAddressTableModel()->labelForAddress(sWalletAddress); if (sWalletLabel.isEmpty()) { sWalletLabel = tr("(no label)"); } if (treeMode) { // wallet address ui->treeWidget->addTopLevelItem(itemWalletAddress); itemWalletAddress->setFlags(flgTristate); itemWalletAddress->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); // label itemWalletAddress->setText(COLUMN_LABEL, sWalletLabel); // address itemWalletAddress->setText(COLUMN_ADDRESS, sWalletAddress); } Amount nSum = Amount::zero(); int nChildren = 0; for (const auto &outpair : coins.second) { const COutPoint &output = std::get<0>(outpair); const interfaces::WalletTxOut &out = std::get<1>(outpair); nSum += out.txout.nValue; nChildren++; CCoinControlWidgetItem *itemOutput; if (treeMode) { itemOutput = new CCoinControlWidgetItem(itemWalletAddress); } else { itemOutput = new CCoinControlWidgetItem(ui->treeWidget); } itemOutput->setFlags(flgCheckbox); itemOutput->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); // address CTxDestination outputAddress; QString sAddress = ""; if (ExtractDestination(out.txout.scriptPubKey, outputAddress)) { sAddress = QString::fromStdString( EncodeCashAddr(outputAddress, model->getChainParams())); // if listMode or change => show bitcoin address. In tree mode, // address is not shown again for direct wallet address outputs if (!treeMode || (!(sAddress == sWalletAddress))) { itemOutput->setText(COLUMN_ADDRESS, sAddress); } } // label if (!(sAddress == sWalletAddress)) { // change tooltip from where the change comes from itemOutput->setToolTip(COLUMN_LABEL, tr("change from %1 (%2)") .arg(sWalletLabel) .arg(sWalletAddress)); itemOutput->setText(COLUMN_LABEL, tr("(change)")); } else if (!treeMode) { QString sLabel = model->getAddressTableModel()->labelForAddress(sAddress); if (sLabel.isEmpty()) { sLabel = tr("(no label)"); } itemOutput->setText(COLUMN_LABEL, sLabel); } // amount itemOutput->setText( COLUMN_AMOUNT, BitcoinUnits::format(nDisplayUnit, out.txout.nValue)); // padding so that sorting works correctly itemOutput->setData( COLUMN_AMOUNT, Qt::UserRole, QVariant(qlonglong(out.txout.nValue / SATOSHI))); // date itemOutput->setText(COLUMN_DATE, GUIUtil::dateTimeStr(out.time)); itemOutput->setData(COLUMN_DATE, Qt::UserRole, QVariant((qlonglong)out.time)); // confirmations itemOutput->setText(COLUMN_CONFIRMATIONS, QString::number(out.depth_in_main_chain)); itemOutput->setData(COLUMN_CONFIRMATIONS, Qt::UserRole, QVariant((qlonglong)out.depth_in_main_chain)); // transaction id itemOutput->setData( COLUMN_ADDRESS, TxIdRole, QString::fromStdString(output.GetTxId().GetHex())); // vout index itemOutput->setData(COLUMN_ADDRESS, VOutRole, output.GetN()); // disable locked coins if (model->wallet().isLockedCoin(output)) { // just to be sure - coinControl()->UnSelect(output); + m_coin_control.UnSelect(output); itemOutput->setDisabled(true); itemOutput->setIcon( COLUMN_CHECKBOX, platformStyle->SingleColorIcon(":/icons/lock_closed")); } // set checkbox - if (coinControl()->IsSelected(output)) { + if (m_coin_control.IsSelected(output)) { itemOutput->setCheckState(COLUMN_CHECKBOX, Qt::Checked); } } // amount if (treeMode) { itemWalletAddress->setText(COLUMN_CHECKBOX, "(" + QString::number(nChildren) + ")"); itemWalletAddress->setText( COLUMN_AMOUNT, BitcoinUnits::format(nDisplayUnit, nSum)); itemWalletAddress->setData(COLUMN_AMOUNT, Qt::UserRole, QVariant(qlonglong(nSum / SATOSHI))); } } // expand all partially selected if (treeMode) { for (int i = 0; i < ui->treeWidget->topLevelItemCount(); i++) { if (ui->treeWidget->topLevelItem(i)->checkState(COLUMN_CHECKBOX) == Qt::PartiallyChecked) { ui->treeWidget->topLevelItem(i)->setExpanded(true); } } } // sort view sortView(sortColumn, sortOrder); ui->treeWidget->setEnabled(true); } diff --git a/src/qt/coincontroldialog.h b/src/qt/coincontroldialog.h index a6532c07f..19fa3e784 100644 --- a/src/qt/coincontroldialog.h +++ b/src/qt/coincontroldialog.h @@ -1,115 +1,115 @@ // 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_COINCONTROLDIALOG_H #define BITCOIN_QT_COINCONTROLDIALOG_H #include #include #include #include #include #include #include #include #include class PlatformStyle; class WalletModel; class CCoinControl; class COutPoint; namespace Ui { class CoinControlDialog; } #define ASYMP_UTF8 "\xE2\x89\x88" class CCoinControlWidgetItem : public QTreeWidgetItem { public: explicit CCoinControlWidgetItem(QTreeWidget *parent, int type = Type) : QTreeWidgetItem(parent, type) {} explicit CCoinControlWidgetItem(int type = Type) : QTreeWidgetItem(type) {} explicit CCoinControlWidgetItem(QTreeWidgetItem *parent, int type = Type) : QTreeWidgetItem(parent, type) {} bool operator<(const QTreeWidgetItem &other) const override; }; class CoinControlDialog : public QDialog { Q_OBJECT public: - explicit CoinControlDialog(const PlatformStyle *platformStyle, + explicit CoinControlDialog(CCoinControl &coin_control, WalletModel *model, + const PlatformStyle *platformStyle, QWidget *parent = nullptr); ~CoinControlDialog(); - void setModel(WalletModel *model); - // static because also called from sendcoinsdialog - static void updateLabels(WalletModel *, QDialog *); + static void updateLabels(CCoinControl &m_coin_control, WalletModel *, + QDialog *); static QList payAmounts; - static CCoinControl *coinControl(); static bool fSubtractFeeFromAmount; private: Ui::CoinControlDialog *ui; + CCoinControl &m_coin_control; WalletModel *model; int sortColumn; Qt::SortOrder sortOrder; QMenu *contextMenu; QTreeWidgetItem *contextMenuItem; QAction *copyTransactionHashAction; QAction *lockAction; QAction *unlockAction; const PlatformStyle *platformStyle; void sortView(int, Qt::SortOrder); void updateView(); enum { COLUMN_CHECKBOX = 0, COLUMN_AMOUNT, COLUMN_LABEL, COLUMN_ADDRESS, COLUMN_DATE, COLUMN_CONFIRMATIONS, }; enum { TxIdRole = Qt::UserRole, VOutRole }; friend class CCoinControlWidgetItem; static COutPoint buildOutPoint(const QTreeWidgetItem *item); private Q_SLOTS: void showMenu(const QPoint &); void copyAmount(); void copyLabel(); void copyAddress(); void copyTransactionHash(); void lockCoin(); void unlockCoin(); void clipboardQuantity(); void clipboardAmount(); void clipboardFee(); void clipboardAfterFee(); void clipboardBytes(); void clipboardLowOutput(); void clipboardChange(); void radioTreeMode(bool); void radioListMode(bool); void viewItemChanged(QTreeWidgetItem *, int); void headerSectionClicked(int); void buttonBoxClicked(QAbstractButton *); void buttonSelectAllClicked(); void updateLabelLocked(); }; #endif // BITCOIN_QT_COINCONTROLDIALOG_H diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 67bd2eb1f..dc9c4bf69 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -1,1041 +1,1028 @@ // 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), + model(_model), m_coin_control(new CCoinControl), + 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)); } 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); 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); Amount requiredFee = model->wallet().getRequiredFee(1000); ui->customFee->SetMinValue(requiredFee); if (ui->customFee->value() < requiredFee) { ui->customFee->setValue(requiredFee); } ui->customFee->setSingleStep(requiredFee); updateFeeSectionControls(); 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)); 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); + updateCoinControlState(*m_coin_control); - prepareStatus = model->prepareTransaction(*m_current_transaction, ctrl); + prepareStatus = + model->prepareTransaction(*m_current_transaction, *m_coin_control); // 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(); + m_coin_control->UnSelectAll(); coinControlUpdateLabels(); } fNewRecipientAllowed = true; m_current_transaction.reset(); } void SendCoinsDialog::clear() { m_current_transaction.reset(); // Clear coin control settings - CoinControlDialog::coinControl()->UnSelectAll(); + m_coin_control->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()); 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(); + m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled(); // Calculate available amount to send. - Amount amount = model->wallet().getAvailableBalance(coin_control); + Amount amount = model->wallet().getAvailableBalance(*m_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::updateFeeSectionControls() { ui->labelSmartFee->setEnabled(ui->radioSmartFee->isChecked()); ui->labelSmartFee2->setEnabled(ui->radioSmartFee->isChecked()); ui->labelFeeEstimation->setEnabled(ui->radioSmartFee->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::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); + updateCoinControlState(*m_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)); + m_coin_control->m_feerate.reset(); + CFeeRate feeRate(model->wallet().getMinimumFee(1000, *m_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(); + m_coin_control->SetNull(); } coinControlUpdateLabels(); } // Coin Control: button inputs -> show actual coin control dialog void SendCoinsDialog::coinControlButtonClicked() { - CoinControlDialog dlg(platformStyle); - dlg.setModel(model); + CoinControlDialog dlg(*m_coin_control, model, platformStyle); dlg.exec(); coinControlUpdateLabels(); } // Coin Control: checkbox custom change address void SendCoinsDialog::coinControlChangeChecked(int state) { if (state == Qt::Unchecked) { - CoinControlDialog::coinControl()->destChange = CNoDestination(); + m_coin_control->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(); + m_coin_control->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; + m_coin_control->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; + m_coin_control->destChange = dest; } } } } // Coin Control: update labels void SendCoinsDialog::coinControlUpdateLabels() { if (!model || !model->getOptionsModel()) { return; } - updateCoinControlState(*CoinControlDialog::coinControl()); + updateCoinControlState(*m_coin_control); // 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()) { + if (m_coin_control->HasSelected()) { // actual coin control calculation - CoinControlDialog::updateLabels(model, this); + CoinControlDialog::updateLabels(*m_coin_control, 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 6e7c915b7..16aa9ea9e 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -1,138 +1,140 @@ // 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 CCoinControl; 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_coin_control; 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 updateFeeSectionControls(); 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() override; private Q_SLOTS: void countDown(); void updateYesButton(); private: QAbstractButton *yesButton; QTimer countDownTimer; int secDelay; QString confirmButtonText; }; #endif // BITCOIN_QT_SENDCOINSDIALOG_H