From e17906a6a668753b136c9bd8e28369613cd671df Mon Sep 17 00:00:00 2001 From: jamescowens Date: Wed, 5 May 2021 14:23:33 -0400 Subject: [PATCH] Implement consolidateunspent wizard This implements a three step wizard that leads the user through the process of selecting inputs, selecting a destination, and then reviewing the overall transaction. Select Inputs: The select inputs screen uses similar code to that in coincontroldialog to support the consolidate button there. Pointers to the coincontrol data structures constructed in sendcoinsdialog are passed into both coincontrol and the consolidateunspentwizard to faciliate using the underlying machinery in a unified manner. This is possible because both coincontrol and consolidateunspendwizard are called with the sendcoinsdialog and are modal. Note that there is a stop sign and the next button is disabled if more than 600 outputs are selected. The next button is also disabled if less than 2 outputs are selected (as it makes no sense to consolidate when there is no consolidation achievable based on the selection). If a filter operation is applied based on the filter criteria, and that criteria would result in more than 600 inputs being selected, the selection is reduced to 600 inputs and a warning triangle is presented. Select Destination Address: If all of the inputs selected in the prior page are from one address, then that address will already be preselected (but allow the opportunity for it to be changed by the user prior to pressing next). If the inputs selected correspond to more than one address, an address will NOT be pre-selected, and the user will be required to pick an address to proceed. The next button will be disabled until a valid address is selected for the destination. Confirmation: The final screen presents the details of the intended transaction for review by the user. When the "Finish" button is pressed, the transaction to send is filled in in the send dialog sreen, and the user can review the details again if desired and press the send button to send. --- src/Makefile.qt.include | 16 + src/qt/coincontroldialog.cpp | 25 +- src/qt/coincontroldialog.h | 12 +- src/qt/consolidateunspentdialog.h | 4 +- src/qt/consolidateunspentwizard.cpp | 59 ++ src/qt/consolidateunspentwizard.h | 56 ++ ...dateunspentwizardselectdestinationpage.cpp | 124 +++ ...lidateunspentwizardselectdestinationpage.h | 34 + ...nsolidateunspentwizardselectinputspage.cpp | 719 ++++++++++++++++++ ...consolidateunspentwizardselectinputspage.h | 91 +++ src/qt/consolidateunspentwizardsendpage.cpp | 53 ++ src/qt/consolidateunspentwizardsendpage.h | 38 + src/qt/forms/consolidateunspentwizard.ui | 69 ++ ...idateunspentwizardselectdestinationpage.ui | 132 ++++ ...onsolidateunspentwizardselectinputspage.ui | 371 +++++++++ .../forms/consolidateunspentwizardsendpage.ui | 119 +++ src/qt/forms/sendcoinsdialog.ui | 9 +- src/qt/sendcoinsdialog.cpp | 52 +- src/qt/sendcoinsdialog.h | 3 + 19 files changed, 1956 insertions(+), 30 deletions(-) create mode 100644 src/qt/consolidateunspentwizard.cpp create mode 100644 src/qt/consolidateunspentwizard.h create mode 100644 src/qt/consolidateunspentwizardselectdestinationpage.cpp create mode 100644 src/qt/consolidateunspentwizardselectdestinationpage.h create mode 100644 src/qt/consolidateunspentwizardselectinputspage.cpp create mode 100644 src/qt/consolidateunspentwizardselectinputspage.h create mode 100644 src/qt/consolidateunspentwizardsendpage.cpp create mode 100644 src/qt/consolidateunspentwizardsendpage.h create mode 100644 src/qt/forms/consolidateunspentwizard.ui create mode 100644 src/qt/forms/consolidateunspentwizardselectdestinationpage.ui create mode 100644 src/qt/forms/consolidateunspentwizardselectinputspage.ui create mode 100644 src/qt/forms/consolidateunspentwizardsendpage.ui diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 2c7aa67182..091f8ca1d4 100755 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -75,6 +75,10 @@ QT_FORMS_UI = \ qt/forms/aboutdialog.ui \ qt/forms/coincontroldialog.ui \ qt/forms/consolidateunspentdialog.ui \ + qt/forms/consolidateunspentwizard.ui \ + qt/forms/consolidateunspentwizardselectdestinationpage.ui \ + qt/forms/consolidateunspentwizardselectinputspage.ui \ + qt/forms/consolidateunspentwizardsendpage.ui \ qt/forms/diagnosticsdialog.ui \ qt/forms/optionsdialog.ui \ qt/forms/rpcconsole.ui \ @@ -113,6 +117,10 @@ QT_MOC_CPP = \ qt/moc_coincontroldialog.cpp \ qt/moc_coincontroltreewidget.cpp \ qt/moc_consolidateunspentdialog.cpp \ + qt/moc_consolidateunspentwizard.cpp \ + qt/moc_consolidateunspentwizardselectdestinationpage.cpp \ + qt/moc_consolidateunspentwizardselectinputspage.cpp \ + qt/moc_consolidateunspentwizardsendpage.cpp \ qt/moc_csvmodelwriter.cpp \ qt/moc_diagnosticsdialog.cpp \ qt/moc_editaddressdialog.cpp \ @@ -184,6 +192,10 @@ GRIDCOINRESEARCH_QT_H = \ qt/coincontroldialog.h \ qt/coincontroltreewidget.h \ qt/consolidateunspentdialog.h \ + qt/consolidateunspentwizard.h \ + qt/consolidateunspentwizardselectdestinationpage.h \ + qt/consolidateunspentwizardselectinputspage.h \ + qt/consolidateunspentwizardsendpage.h \ qt/csvmodelwriter.h \ qt/decoration.h \ qt/diagnosticsdialog.h \ @@ -247,6 +259,10 @@ GRIDCOINRESEARCH_QT_CPP = \ qt/coincontroldialog.cpp \ qt/coincontroltreewidget.cpp \ qt/consolidateunspentdialog.cpp \ + qt/consolidateunspentwizard.cpp \ + qt/consolidateunspentwizardselectdestinationpage.cpp \ + qt/consolidateunspentwizardselectinputspage.cpp \ + qt/consolidateunspentwizardsendpage.cpp \ qt/csvmodelwriter.cpp \ qt/decoration.cpp \ qt/diagnosticsdialog.cpp \ diff --git a/src/qt/coincontroldialog.cpp b/src/qt/coincontroldialog.cpp index dd605f2104..f92174bf6a 100644 --- a/src/qt/coincontroldialog.cpp +++ b/src/qt/coincontroldialog.cpp @@ -25,13 +25,13 @@ #include using namespace std; -QList CoinControlDialog::payAmounts; -CCoinControl* CoinControlDialog::coinControl = new CCoinControl(); -CoinControlDialog::CoinControlDialog(QWidget *parent) : +CoinControlDialog::CoinControlDialog(QWidget *parent, CCoinControl *coinControl, QList *payAmounts) : QDialog(parent), m_inputSelectionLimit(GetMaxInputsForConsolidationTxn()), ui(new Ui::CoinControlDialog), + coinControl(coinControl), + payAmounts(payAmounts), model(0) { ui->setupUi(this); @@ -158,7 +158,7 @@ void CoinControlDialog::setModel(WalletModel *model) { updateView(); //updateLabelLocked(); - CoinControlDialog::updateLabels(model, this); + CoinControlDialog::updateLabels(model, coinControl, payAmounts, this); } } @@ -226,7 +226,7 @@ void CoinControlDialog::buttonSelectAllClicked() ui->selectAllPushButton->setText("Select None"); } - CoinControlDialog::updateLabels(model, this); + CoinControlDialog::updateLabels(model, coinControl, payAmounts, this); showHideConsolidationReadyToSend(); } @@ -353,7 +353,7 @@ bool CoinControlDialog::filterInputsByValue(const bool& less, const CAmount& inp // Reenable update signals. ui->treeWidget->setEnabled(true); - CoinControlDialog::updateLabels(model, this); + CoinControlDialog::updateLabels(model, coinControl, payAmounts, this); // If the number of inputs selected was limited, then true is returned. return culled_inputs; @@ -596,7 +596,9 @@ void CoinControlDialog::viewItemChanged(QTreeWidgetItem* item, int column) // selection changed -> update labels if (ui->treeWidget->isEnabled()) // do not update on every click for (un)select all - CoinControlDialog::updateLabels(model, this); + { + CoinControlDialog::updateLabels(model, coinControl, payAmounts, this); + } } showHideConsolidationReadyToSend(); @@ -633,7 +635,10 @@ QString CoinControlDialog::getPriorityLabel(double dPriority) else ui->labelLocked->setVisible(false); }*/ -void CoinControlDialog::updateLabels(WalletModel *model, QDialog* dialog) +void CoinControlDialog::updateLabels(WalletModel *model, + CCoinControl *coinControl, + QList* payAmounts, + QDialog* dialog) { if (!model) return; @@ -642,7 +647,7 @@ void CoinControlDialog::updateLabels(WalletModel *model, QDialog* dialog) bool fLowOutput = false; bool fDust = false; CTransaction txDummy; - foreach(const qint64 &amount, CoinControlDialog::payAmounts) + foreach(const qint64 &amount, *payAmounts) { nPayAmount += amount; @@ -704,7 +709,7 @@ void CoinControlDialog::updateLabels(WalletModel *model, QDialog* dialog) if (nQuantity > 0) { // Bytes - nBytes = nBytesInputs + ((CoinControlDialog::payAmounts.size() > 0 ? CoinControlDialog::payAmounts.size() + 1 : 2) * 34) + 10; // always assume +1 output for change here + nBytes = nBytesInputs + ((payAmounts->size() > 0 ? payAmounts->size() + 1 : 2) * 34) + 10; // always assume +1 output for change here // Priority dPriority = dPriorityInputs / nBytes; diff --git a/src/qt/coincontroldialog.h b/src/qt/coincontroldialog.h index b948181bef..1e511936d9 100644 --- a/src/qt/coincontroldialog.h +++ b/src/qt/coincontroldialog.h @@ -24,17 +24,19 @@ class CoinControlDialog : public QDialog Q_OBJECT public: - explicit CoinControlDialog(QWidget *parent = 0); + explicit CoinControlDialog(QWidget *parent = 0, + CCoinControl *coinControl = nullptr, + QList *payAmounts = nullptr); ~CoinControlDialog(); void setModel(WalletModel *model); // static because also called from sendcoinsdialog - static void updateLabels(WalletModel*, QDialog*); + static void updateLabels(WalletModel*, CCoinControl*, QList*, QDialog*); static QString getPriorityLabel(double); - static QList payAmounts; - static CCoinControl *coinControl; + //static QList payAmounts; + //static CCoinControl *coinControl; // This is based on what will guarantee a successful transaction. const size_t m_inputSelectionLimit; @@ -47,6 +49,8 @@ public slots: private: Ui::CoinControlDialog *ui; + CCoinControl *coinControl; + QList *payAmounts; WalletModel *model; int sortColumn; Qt::SortOrder sortOrder; diff --git a/src/qt/consolidateunspentdialog.h b/src/qt/consolidateunspentdialog.h index 440e4f160b..652777b165 100644 --- a/src/qt/consolidateunspentdialog.h +++ b/src/qt/consolidateunspentdialog.h @@ -3,7 +3,9 @@ #include #include +#include #include +#include namespace Ui { class ConsolidateUnspentDialog; @@ -14,7 +16,7 @@ class ConsolidateUnspentDialog : public QDialog Q_OBJECT public: - explicit ConsolidateUnspentDialog(QWidget *parent = 0, size_t inputSelectionLimit = 600); + explicit ConsolidateUnspentDialog(QWidget *parent = nullptr, size_t inputSelectionLimit = 600); ~ConsolidateUnspentDialog(); void SetAddressList(const std::map& addressList); diff --git a/src/qt/consolidateunspentwizard.cpp b/src/qt/consolidateunspentwizard.cpp new file mode 100644 index 0000000000..9afe177fea --- /dev/null +++ b/src/qt/consolidateunspentwizard.cpp @@ -0,0 +1,59 @@ +#include "coincontroldialog.h" +#include "consolidateunspentwizard.h" +#include "consolidateunspentdialog.h" +#include "ui_consolidateunspentwizard.h" + +#include "util.h" + + +ConsolidateUnspentWizard::ConsolidateUnspentWizard(QWidget *parent, + CCoinControl *coinControl, + QList *payAmounts, + size_t inputSelectionLimit) : + QWizard(parent), + ui(new Ui::ConsolidateUnspentWizard), + coinControl(coinControl), + payAmounts(payAmounts), + m_inputSelectionLimit(inputSelectionLimit) +{ + ui->setupUi(this); + this->setStartId(SelectInputsPage); + + ui->selectInputsPage->setCoinControl(coinControl); + ui->selectInputsPage->setPayAmounts(payAmounts); + + connect(this, SIGNAL(setModelSignal(WalletModel*)), ui->selectInputsPage, SLOT(setModel(WalletModel*))); + connect(this, SIGNAL(setModelSignal(WalletModel*)), ui->sendPage, SLOT(setModel(WalletModel*))); + + connect(ui->selectInputsPage, SIGNAL(setAddressListSignal(std::map)), + ui->selectDestinationPage, SLOT(SetAddressList(const std::map))); + + connect(ui->selectInputsPage, SIGNAL(setDefaultAddressSignal(QString)), + ui->selectDestinationPage, SLOT(setDefaultAddressSelection(QString))); + + connect(this->button(QWizard::FinishButton), SIGNAL(clicked()), ui->sendPage, SLOT(onFinishButtonClicked())); + connect(ui->sendPage, SIGNAL(selectedConsolidationRecipientSignal(SendCoinsRecipient)), + this, SIGNAL(selectedConsolidationRecipientSignal(SendCoinsRecipient))); +} + +ConsolidateUnspentWizard::~ConsolidateUnspentWizard() +{ + delete ui; +} + +void ConsolidateUnspentWizard::accept() +{ + QDialog::accept(); + //emit sendConsolidationTransactionSignal(); +} + +void ConsolidateUnspentWizard::setModel(WalletModel *model) +{ + this->model = model; + emit setModelSignal(model); +} + +WalletModel* ConsolidateUnspentWizard::getModel() +{ + return this->model; +} diff --git a/src/qt/consolidateunspentwizard.h b/src/qt/consolidateunspentwizard.h new file mode 100644 index 0000000000..a6142c7cda --- /dev/null +++ b/src/qt/consolidateunspentwizard.h @@ -0,0 +1,56 @@ +#ifndef CONSOLIDATEUNSPENTWIZARD_H +#define CONSOLIDATEUNSPENTWIZARD_H + +#include "walletmodel.h" + +#include +#include +#include +#include +#include + +namespace Ui { + class ConsolidateUnspentWizard; +} + +class CoinControlDialog; + +class ConsolidateUnspentWizard : public QWizard +{ + Q_OBJECT + +public: + enum Pages + { + SelectInputsPage, + SelectDestinationPage, + SendPage + }; + + explicit ConsolidateUnspentWizard(QWidget *parent = nullptr, + CCoinControl *coinControl = nullptr, + QList *payAmounts = nullptr, + size_t inputSelectionLimit = 600); + ~ConsolidateUnspentWizard(); + + void setModel(WalletModel *model); + WalletModel* getModel(); + + void accept() override; + +signals: + void setModelSignal(WalletModel*); + void passCoinControlSignal(CCoinControl*); + void selectedConsolidationRecipientSignal(SendCoinsRecipient); + void sendConsolidationTransactionSignal(); + +private: + Ui::ConsolidateUnspentWizard *ui; + CCoinControl *coinControl; + QList *payAmounts; + WalletModel *model; + + size_t m_inputSelectionLimit; +}; + +#endif // CONSOLIDATEUNSPENTWIZARD_H diff --git a/src/qt/consolidateunspentwizardselectdestinationpage.cpp b/src/qt/consolidateunspentwizardselectdestinationpage.cpp new file mode 100644 index 0000000000..c4a14c226e --- /dev/null +++ b/src/qt/consolidateunspentwizardselectdestinationpage.cpp @@ -0,0 +1,124 @@ +#include "consolidateunspentwizardselectdestinationpage.h" +#include "ui_consolidateunspentwizardselectdestinationpage.h" + +#include "util.h" + +ConsolidateUnspentWizardSelectDestinationPage::ConsolidateUnspentWizardSelectDestinationPage(QWidget *parent) : + QWizardPage(parent), + ui(new Ui::ConsolidateUnspentWizardSelectDestinationPage) +{ + ui->setupUi(this); + + QStringList headerLabels; + + // This should not be necessary because this is defined in the .ui file but for some reason only numbers + // are showing up without it. + headerLabels << tr("Label") << tr("Address"); + ui->addressTableWidget->setHorizontalHeaderLabels(headerLabels); + + ui->addressTableWidget->setSelectionMode(QAbstractItemView::SingleSelection); + + // destination address selection + connect(ui->addressTableWidget, SIGNAL(itemSelectionChanged()), this, SLOT(addressSelectionChanged())); + + ui->isCompleteCheckBox->hide(); + + // This is to provide a convenient way to populate the fields shown on the last page ("send" screen). + registerField("selectedAddressLabelField", ui->selectedAddressLabel, "text", "updateFieldsSignal()"); + registerField("selectedAddressField", ui->selectedAddress, "text", "updateFieldsSignal()"); + + //This is used to control the disable/enable of the next button on this page. + registerField("isCompleteSelectDestination*", ui->isCompleteCheckBox); +} + +ConsolidateUnspentWizardSelectDestinationPage::~ConsolidateUnspentWizardSelectDestinationPage() +{ + delete ui; +} + +// ----------------------------------------------------------------------------- address - label +void ConsolidateUnspentWizardSelectDestinationPage::SetAddressList(const std::map addressList) +{ + ui->addressTableWidget->setSortingEnabled(false); + + ui->addressTableWidget->clear(); + + int row = 0; + for (const auto& iter : addressList) + { + ui->addressTableWidget->insertRow(row); + + QTableWidgetItem* label = new QTableWidgetItem(iter.second); + QTableWidgetItem* address = new QTableWidgetItem(iter.first); + + if (label != nullptr) ui->addressTableWidget->setItem(row, 0, label); + if (address != nullptr) ui->addressTableWidget->setItem(row, 1, address); + + ++row; + } +} + +void ConsolidateUnspentWizardSelectDestinationPage::setDefaultAddressSelection(QString address) +{ + if (!address.size()) + { + ui->addressTableWidget->clearSelection(); + + LogPrint(BCLog::LogFlags::QT, "INFO: %s: Cleared (default) address selection", __func__); + + ui->isCompleteCheckBox->setChecked(false); + + return; + } + + QList defaultAddress = ui->addressTableWidget->findItems(address, Qt::MatchExactly); + + defaultAddress[0]->setSelected(true); + + LogPrint(BCLog::LogFlags::QT, "INFO: %s: Set default address to %s, QTableWidgetItem %s", + __func__, + address.toStdString(), + defaultAddress[0]->text().toStdString()); + + ui->addressTableWidget->setCurrentItem(defaultAddress[0]); + + LogPrintf("INFO: %s: currentRow = %i", __func__, ui->addressTableWidget->currentRow()); + + emit updateFieldsSignal(); +} + +void ConsolidateUnspentWizardSelectDestinationPage::addressSelectionChanged() +{ + + if (!ui->addressTableWidget->selectedItems().size()) + { + ui->selectedAddressLabel->setText(QString()); + ui->selectedAddress->setText(QString()); + + ui->isCompleteCheckBox->setChecked(false); + + return; + } + + ui->addressTableWidget->selectedItems()[0]->row(); + + int selectedRow = ui->addressTableWidget->selectedItems()[0]->row(); + + if (selectedRow < 0) return; + + QTableWidgetItem* selectedLabel = ui->addressTableWidget->item(selectedRow, 0); + QTableWidgetItem* selectedAddress = ui->addressTableWidget->item(selectedRow, 1); + + ui->selectedAddressLabel->setText(selectedLabel->text()); + ui->selectedAddress->setText(selectedAddress->text()); + + m_selectedDestinationAddress = std::make_pair(selectedLabel->text(), selectedAddress->text()); + + LogPrint(BCLog::LogFlags::QT, "INFO: %s: Label %, Address %s selected.", __func__, + m_selectedDestinationAddress.first.toStdString(), + m_selectedDestinationAddress.second.toStdString()); + + ui->isCompleteCheckBox->setChecked(true); + + emit updateFieldsSignal(); +} diff --git a/src/qt/consolidateunspentwizardselectdestinationpage.h b/src/qt/consolidateunspentwizardselectdestinationpage.h new file mode 100644 index 0000000000..62bf1e8dee --- /dev/null +++ b/src/qt/consolidateunspentwizardselectdestinationpage.h @@ -0,0 +1,34 @@ +#ifndef CONSOLIDATEUNSPENTWIZARDSELECTDESTINATIONPAGE_H +#define CONSOLIDATEUNSPENTWIZARDSELECTDESTINATIONPAGE_H + +#include + +namespace Ui { + class ConsolidateUnspentWizardSelectDestinationPage; +} + +class ConsolidateUnspentWizardSelectDestinationPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit ConsolidateUnspentWizardSelectDestinationPage(QWidget *parent = nullptr); + ~ConsolidateUnspentWizardSelectDestinationPage(); + +signals: + void updateFieldsSignal(); + +public slots: + void SetAddressList(const std::map addressList); + void setDefaultAddressSelection(QString address); + +private: + Ui::ConsolidateUnspentWizardSelectDestinationPage *ui; + + std::pair m_selectedDestinationAddress; + +private slots: + void addressSelectionChanged(); +}; + +#endif // CONSOLIDATEUNSPENTWIZARDSELECTDESTINATIONPAGE_H diff --git a/src/qt/consolidateunspentwizardselectinputspage.cpp b/src/qt/consolidateunspentwizardselectinputspage.cpp new file mode 100644 index 0000000000..0f61295a38 --- /dev/null +++ b/src/qt/consolidateunspentwizardselectinputspage.cpp @@ -0,0 +1,719 @@ +#include "coincontroldialog.h" +#include "consolidateunspentwizardselectinputspage.h" +#include "ui_consolidateunspentwizardselectinputspage.h" + +#include "init.h" +#include "bitcoinunits.h" +#include "addresstablemodel.h" +#include "optionsmodel.h" +#include "policy/policy.h" +#include "policy/fees.h" +#include "validation.h" +#include "wallet/coincontrol.h" +#include "consolidateunspentdialog.h" + +using namespace std; + +ConsolidateUnspentWizardSelectInputsPage::ConsolidateUnspentWizardSelectInputsPage(QWidget *parent) : + QWizardPage(parent), + ui(new Ui::ConsolidateUnspentWizardSelectInputsPage) +{ + m_InputSelectionLimit = GetMaxInputsForConsolidationTxn(); + + ui->setupUi(this); + + // toggle tree/list mode + connect(ui->treeModeRadioButton, SIGNAL(toggled(bool)), this, SLOT(treeModeRadioButton(bool))); + connect(ui->listModeRadioButton, SIGNAL(toggled(bool)), this, SLOT(listModeRadioButton(bool))); + + // click on checkbox + connect(ui->treeWidget, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(viewItemChanged(QTreeWidgetItem*, int))); + + // click on header + ui->treeWidget->header()->setSectionsClickable(true); + connect(ui->treeWidget->header(), SIGNAL(sectionClicked(int)), this, SLOT(headerSectionClicked(int))); + + // (un)select all + connect(ui->selectAllPushButton, SIGNAL(clicked()), this, SLOT(buttonSelectAllClicked())); + + // filter/consolidate button interaction + connect(ui->maxMinOutputValue, SIGNAL(textChanged()), this, SLOT(maxMinOutputValueChanged())); + + // filter mode + connect(ui->filterModePushButton, SIGNAL(clicked()), this, SLOT(buttonFilterModeClicked())); + + // filter + connect(ui->filterPushButton, SIGNAL(clicked()), this, SLOT(buttonFilterClicked())); + + ui->treeWidget->setColumnWidth(COLUMN_CHECKBOX, 150); + ui->treeWidget->setColumnWidth(COLUMN_AMOUNT, 170); + ui->treeWidget->setColumnWidth(COLUMN_LABEL, 200); + ui->treeWidget->setColumnWidth(COLUMN_ADDRESS, 290); + ui->treeWidget->setColumnWidth(COLUMN_DATE, 110); + ui->treeWidget->setColumnWidth(COLUMN_CONFIRMATIONS, 100); + ui->treeWidget->setColumnWidth(COLUMN_PRIORITY, 100); + ui->treeWidget->setColumnHidden(COLUMN_TXHASH, true); // store transacton hash in this column, but don't show it + ui->treeWidget->setColumnHidden(COLUMN_VOUT_INDEX, true); // store vout index in this column, but don't show it + ui->treeWidget->setColumnHidden(COLUMN_AMOUNT_INT64, true); // store amount int64_t in this column, but don't show it + ui->treeWidget->setColumnHidden(COLUMN_PRIORITY_INT64, true); // store priority int64_t in this column, but don't show it + ui->treeWidget->setColumnHidden(COLUMN_CHANGE_BOOL, true); // store change flag but don't show it + + // This is to provide a convenient way to populate the fields shown on the last page ("send" screen). + registerField("quantityField", ui->quantityLabel, "text", "updateFieldsSignal()"); + registerField("feeField", ui->feeLabel, "text", "updateFieldsSignal()"); + registerField("afterFeeAmountField", ui->afterFeeLabel, "text", "updateFieldsSignal()"); + + //This is used to control the disable/enable of the next button on this page. + registerField("isCompleteSelectInputs*", ui->isCompleteCheckBox); + + // default view is sorted by amount desc + sortView(COLUMN_AMOUNT_INT64, Qt::DescendingOrder); + + ui->outputLimitWarningIconLabel->setToolTip(tr("Note: The number of inputs selected for consolidation has been " + "limited to %1 to prevent a transaction failure due to too many " + "inputs.").arg(m_InputSelectionLimit)); + ui->outputLimitStopIconLabel->setToolTip(tr("Note: The number of inputs selected for consolidation is currently more " + "than the limit of %1. Please use the filter or manual selection to reduce " + "the number of inputs to %1 or less to prevent a transaction failure due to " + "too many inputs.").arg(m_InputSelectionLimit)); + + ui->outputLimitWarningIconLabel->setVisible(false); + ui->outputLimitStopIconLabel->setVisible(false); + + ui->isCompleteCheckBox->hide(); +} + +ConsolidateUnspentWizardSelectInputsPage::~ConsolidateUnspentWizardSelectInputsPage() +{ + delete ui; +} + +void ConsolidateUnspentWizardSelectInputsPage::setModel(WalletModel *model) +{ + this->model = model; + + if (model && model->getOptionsModel() && model->getAddressTableModel() && coinControl != nullptr) + { + updateView(); + updateLabels(); + } +} + +void ConsolidateUnspentWizardSelectInputsPage::setCoinControl(CCoinControl *coinControl) +{ + this->coinControl = coinControl; +} + +void ConsolidateUnspentWizardSelectInputsPage::setPayAmounts(QList *payAmounts) +{ + this->payAmounts = payAmounts; +} + +// helper function str_pad +QString ConsolidateUnspentWizardSelectInputsPage::strPad(QString s, int nPadLength, QString sPadding) +{ + while (s.length() < nPadLength) + s = sPadding + s; + + return s; +} + +// (un)select all +void ConsolidateUnspentWizardSelectInputsPage::buttonSelectAllClicked() +{ + m_InputSelectionLimitedByFilter = false; + + ui->treeWidget->setEnabled(false); + for (int i = 0; i < ui->treeWidget->topLevelItemCount(); i++) + if (ui->treeWidget->topLevelItem(i)->checkState(COLUMN_CHECKBOX) != m_ToState) + ui->treeWidget->topLevelItem(i)->setCheckState(COLUMN_CHECKBOX, m_ToState); + ui->treeWidget->setEnabled(true); + + if (m_ToState == Qt::Checked) + { + m_ToState = Qt::Unchecked; + } + else + { + m_ToState = Qt::Checked; + } + + if (m_ToState == Qt::Checked) + { + ui->selectAllPushButton->setText("Select All"); + } + else + { + ui->selectAllPushButton->setText("Select None"); + } + + updateLabels(); +} + +void ConsolidateUnspentWizardSelectInputsPage::maxMinOutputValueChanged() +{ + ui->maxMinOutputValue->value(&m_FilterValueValid); +} + +void ConsolidateUnspentWizardSelectInputsPage::buttonFilterModeClicked() +{ + if (m_FilterMode) + { + m_FilterMode = false; + ui->filterModePushButton->setText(">="); + } + else + { + m_FilterMode = true; + ui->filterModePushButton->setText("<="); + } +} + +void ConsolidateUnspentWizardSelectInputsPage::buttonFilterClicked() +{ + m_ViewItemsChangedViaFilter = true; + + m_InputSelectionLimitedByFilter = filterInputsByValue(m_FilterMode, ui->maxMinOutputValue->value(), m_InputSelectionLimit); + + updateLabels(); + + m_ViewItemsChangedViaFilter = false; +} + +bool ConsolidateUnspentWizardSelectInputsPage::filterInputsByValue(const bool& less, const CAmount& inputFilterValue, + const unsigned int& inputSelectionLimit) +{ + + // Disable generating update signals unnecessarily during this filter operation. + ui->treeWidget->setEnabled(false); + + QTreeWidgetItemIterator iter(ui->treeWidget); + + // If less is true, then we are choosing the smallest inputs upward, and so the map comparator needs to be "less than". + // If less is false, then we are choosing the largest inputs downward, and so the map comparator needs to be "greater + // than". + auto comp = [less](CAmount a, CAmount b) + { + if (less) + { + return (a < b); + } + else + { + return (a > b); + } + }; + + std::multimap, decltype(comp)> input_map(comp); + + bool culled_inputs = false; + + while (*iter) + { + CAmount input_value = (*iter)->text(COLUMN_AMOUNT_INT64).toLongLong(); + COutPoint outpoint(uint256S((*iter)->text(COLUMN_TXHASH).toStdString()), (*iter)->text(COLUMN_VOUT_INDEX).toUInt()); + + if ((*iter)->checkState(COLUMN_CHECKBOX) == Qt::Checked) + { + if ((*iter)->text(COLUMN_TXHASH).length() == 64) + { + if ((less && input_value <= inputFilterValue) || (!less && input_value >= inputFilterValue)) + { + input_map.insert(std::make_pair(input_value, std::make_pair(*iter, outpoint))); + } + else + { + (*iter)->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); + coinControl->UnSelect(outpoint); + } + } + } + + ++iter; + } + + // The second loop is to limit the number of selected outputs to the inputCountLimit. + unsigned int input_count = 0; + + for (auto& input : input_map) + { + if (input_count >= inputSelectionLimit) + { + LogPrint(BCLog::LogFlags::QT, "INFO: %s: Culled input %u with value %f.", + __func__, input_count, (double) input.first / COIN); + + if (coinControl->IsSelected(input.second.second.hash, input.second.second.n)) + { + input.second.first->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); + + culled_inputs = true; + coinControl->UnSelect(input.second.second); + } + } + + ++input_count; + } + + // Reenable update signals. + ui->treeWidget->setEnabled(true); + + // If the number of inputs selected was limited, then true is returned. + return culled_inputs; +} + +// treeview: sort +void ConsolidateUnspentWizardSelectInputsPage::sortView(int column, Qt::SortOrder order) +{ + sortColumn = column; + sortOrder = order; + ui->treeWidget->sortItems(column, order); + ui->treeWidget->header()->setSortIndicator((sortColumn == COLUMN_AMOUNT_INT64 ? + COLUMN_AMOUNT : (sortColumn == COLUMN_PRIORITY_INT64 ? + COLUMN_PRIORITY : sortColumn)), + sortOrder); +} + +// treeview: clicked on header +void ConsolidateUnspentWizardSelectInputsPage::headerSectionClicked(int logicalIndex) +{ + if (logicalIndex == COLUMN_CHECKBOX) // click on most left column -> do nothing + { + ui->treeWidget->header()->setSortIndicator((sortColumn == COLUMN_AMOUNT_INT64 ? + COLUMN_AMOUNT : (sortColumn == COLUMN_PRIORITY_INT64 ? + COLUMN_PRIORITY : sortColumn)), + sortOrder); + } + else + { + if (logicalIndex == COLUMN_AMOUNT) // sort by amount + logicalIndex = COLUMN_AMOUNT_INT64; + + if (logicalIndex == COLUMN_PRIORITY) // sort by priority + logicalIndex = COLUMN_PRIORITY_INT64; + + if (sortColumn == logicalIndex) + sortOrder = ((sortOrder == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder); + else + { + sortColumn = logicalIndex; + + // if amount,date,conf,priority then default => desc, else default => asc + sortOrder = ((sortColumn == COLUMN_AMOUNT_INT64 || sortColumn == COLUMN_PRIORITY_INT64 + || sortColumn == COLUMN_DATE || sortColumn == COLUMN_CONFIRMATIONS) ? + Qt::DescendingOrder : Qt::AscendingOrder); + } + + sortView(sortColumn, sortOrder); + } +} + + +// toggle tree mode +void ConsolidateUnspentWizardSelectInputsPage::treeModeRadioButton(bool checked) +{ + if (checked && model) + updateView(); +} + +// toggle list mode +void ConsolidateUnspentWizardSelectInputsPage::listModeRadioButton(bool checked) +{ + if (checked && model) + updateView(); +} + +// checkbox clicked by user +void ConsolidateUnspentWizardSelectInputsPage::viewItemChanged(QTreeWidgetItem* item, int column) +{ + if (!m_ViewItemsChangedViaFilter) m_InputSelectionLimitedByFilter = false; + + if (column == COLUMN_CHECKBOX) + { + // transaction hash is 64 characters (this means its a child node, so its not a parent node in tree mode) + if (item->text(COLUMN_TXHASH).length() == 64) + { + COutPoint outpt(uint256S(item->text(COLUMN_TXHASH).toStdString()), item->text(COLUMN_VOUT_INDEX).toUInt()); + + if (item->checkState(COLUMN_CHECKBOX) == Qt::Unchecked) + { + coinControl->UnSelect(outpt); + } + else if (item->isDisabled()) // locked (this happens if "check all" through parent node) + { + item->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked); + } + else + { + coinControl->Select(outpt); + } + } + + // selection changed -> update labels + if (ui->treeWidget->isEnabled()) + { + // do not update on every click for (un)select all + updateLabels(); + } + } +} + +void ConsolidateUnspentWizardSelectInputsPage::updateLabels() +{ + if (!model) return; + + // nPayAmount + qint64 nPayAmount = 0; + CTransaction txDummy; + for (const auto& amount: *payAmounts) + { + nPayAmount += amount; + + if (amount > 0) + { + CTxOut txout(amount, (CScript)vector(24, 0)); + txDummy.vout.push_back(txout); + } + } + + QString sPriorityLabel = QString(); + int64_t nAmount = 0; + int64_t nPayFee = 0; + int64_t nAfterFee = 0; + int64_t nChange = 0; + unsigned int nBytes = 0; + unsigned int nBytesInputs = 0; + unsigned int nQuantity = 0; + + vector vCoinControl; + vector vOutputs; + coinControl->ListSelected(vCoinControl); + model->getOutputs(vCoinControl, vOutputs); + + for (const auto& out : vOutputs) + { + // Quantity + nQuantity++; + + // Amount + nAmount += out.tx->vout[out.i].nValue; + + // Bytes + CTxDestination address; + if (ExtractDestination(out.tx->vout[out.i].scriptPubKey, address)) + { + CPubKey pubkey; + try { + if (model->getPubKey(std::get(address), pubkey)) + nBytesInputs += (pubkey.IsCompressed() ? 148 : 180); + else + nBytesInputs += 148; // in all error cases, simply assume 148 here + } catch (const std::bad_variant_access&) { + nBytesInputs += 148; + } + } + else nBytesInputs += 148; + } + + // calculation + if (nQuantity > 0) + { + // Bytes - always assume +1 output for change here + nBytes = nBytesInputs + ((payAmounts->size() > 0 ? payAmounts->size() + 1 : 2) * 34) + 10; + + // Fee + int64_t nFee = nTransactionFee * (1 + (int64_t)nBytes / 1000); + + // Min Fee + int64_t nMinFee = GetMinFee(txDummy, 1000, GMF_SEND, nBytes); + + nPayFee = max(nFee, nMinFee); + + if (nPayAmount > 0) + { + nChange = nAmount - nPayFee - nPayAmount; + + // if sub-cent change is required, the fee must be raised to at least CTransaction::nMinTxFee + if (nPayFee < CENT && nChange > 0 && nChange < CENT) + { + if (nChange < CENT) // change < 0.01 => simply move all change to fees + { + nPayFee = nChange; + nChange = 0; + } + else + { + nChange = nChange + nPayFee - CENT; + nPayFee = CENT; + } + } + + if (nChange == 0) nBytes -= 34; + } + + // after fee + nAfterFee = nAmount - nPayFee; + if (nAfterFee < 0) nAfterFee = 0; + } + + // actually update labels + int nDisplayUnit = BitcoinUnits::BTC; + if (model && model->getOptionsModel()) nDisplayUnit = model->getOptionsModel()->getDisplayUnit(); + + // stats + ui->quantityLabel->setText(QString::number(nQuantity)); // Quantity + ui->feeLabel->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nPayFee)); // Fee + ui->afterFeeLabel->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAfterFee)); // After Fee + + std::map addressList; + QString defaultAddress; + unsigned int numberAddressesWhereOutputsChecked = 0; + + for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) + { + QString label = ui->treeWidget->topLevelItem(i)->text(COLUMN_LABEL); + QString address = ui->treeWidget->topLevelItem(i)->text(COLUMN_ADDRESS); + QString change = ui->treeWidget-> topLevelItem(i)->text(COLUMN_CHANGE_BOOL); + + Qt::CheckState state = ui->treeWidget->topLevelItem(i)->checkState(COLUMN_CHECKBOX); + + // If a not unchecked top level item is not a change address and it results in an insert into the m_AddressList + if (!change.toInt() && addressList.insert(std::make_pair(address, label)).second) + { + if (state == Qt::Checked || state == Qt::PartiallyChecked) + { + defaultAddress = label; + + ++numberAddressesWhereOutputsChecked; + } + + if (!addressList.empty()) emit setAddressListSignal(addressList); + } + } + + // This covers the 0 case too, where the default address will be an empty QString. + if (numberAddressesWhereOutputsChecked < 2) + { + // This will be an empty QString if the numberAddressesWhereOutputsChecked equals 0. It will be + // the above defaultAddress if numberAddressesWhereOutputsChecked equals 1. + emit setDefaultAddressSignal(defaultAddress); + } + else + { + // If numberAddressesWhereOutputsChecked is 2 or greater, then clear the default address (i.e. set to + // empty QString. + emit setDefaultAddressSignal(QString()); + } + + // This provids the trigger to update the fields from the labels, since they are QLabels and don't have appropriate + // internal signals. + emit updateFieldsSignal(); + + if (nQuantity < 2) + { + SetOutputWarningStop(InputStatus::INSUFFICIENT_OUTPUTS); + } + else if (nQuantity < m_InputSelectionLimit + || (nQuantity == m_InputSelectionLimit && !m_InputSelectionLimitedByFilter)) + { + SetOutputWarningStop(InputStatus::NORMAL); + } + else if (nQuantity == m_InputSelectionLimit && m_InputSelectionLimitedByFilter) + { + SetOutputWarningStop(InputStatus::WARNING); + } + else if (nQuantity > m_InputSelectionLimit) + { + SetOutputWarningStop(InputStatus::STOP); + } +} + +void ConsolidateUnspentWizardSelectInputsPage::updateView() +{ + bool treeMode = ui->treeModeRadioButton->isChecked(); + + ui->treeWidget->clear(); + ui->treeWidget->setEnabled(false); // performance, otherwise updateLabels would be called for every checked checkbox + ui->treeWidget->setAlternatingRowColors(!treeMode); + QFlags flgCheckbox=Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; + QFlags flgTristate=Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsTristate; + + int nDisplayUnit = BitcoinUnits::BTC; + + if (model && model->getOptionsModel()) + { + nDisplayUnit = model->getOptionsModel()->getDisplayUnit(); + } + + map> mapCoins; + model->listCoins(mapCoins); + + for (auto const& coins : mapCoins) + { + QTreeWidgetItem *itemWalletAddress = new QTreeWidgetItem(); + QString sWalletAddress = coins.first; + QString sWalletLabel = ""; + if (model->getAddressTableModel()) + sWalletLabel = model->getAddressTableModel()->labelForAddress(sWalletAddress); + if (sWalletLabel.length() == 0) + 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); + } + + int64_t nSum = 0; + double dPrioritySum = 0; + int nChildren = 0; + int nInputSum = 0; + + for (auto const& out : coins.second) + { + int nInputSize = 148; // 180 if uncompressed public key + nSum += out.tx->vout[out.i].nValue; + nChildren++; + + QTreeWidgetItem *itemOutput; + if (treeMode) itemOutput = new QTreeWidgetItem(itemWalletAddress); + else itemOutput = new QTreeWidgetItem(ui->treeWidget); + itemOutput->setFlags(flgCheckbox); + itemOutput->setCheckState(COLUMN_CHECKBOX,Qt::Unchecked); + + // address + CTxDestination outputAddress; + QString sAddress = ""; + if (ExtractDestination(out.tx->vout[out.i].scriptPubKey, outputAddress)) + { + sAddress = CBitcoinAddress(outputAddress).ToString().c_str(); + + // 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); + + CPubKey pubkey; + try { + if (model->getPubKey(std::get(outputAddress), pubkey) && !pubkey.IsCompressed()) + nInputSize = 180; + } catch (const std::bad_variant_access&) {} + } + + // 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)")); + itemOutput->setText(COLUMN_CHANGE_BOOL, QString::number(1)); + } + else if (!treeMode) + { + QString sLabel = ""; + if (model->getAddressTableModel()) + sLabel = model->getAddressTableModel()->labelForAddress(sAddress); + if (sLabel.length() == 0) + sLabel = tr("(no label)"); + itemOutput->setText(COLUMN_LABEL, sLabel); + } + + // amount + itemOutput->setText(COLUMN_AMOUNT, BitcoinUnits::format(nDisplayUnit, out.tx->vout[out.i].nValue)); + itemOutput->setText(COLUMN_AMOUNT_INT64, strPad(QString::number(out.tx->vout[out.i].nValue), 15, " ")); // padding so that sorting works correctly + + // date + itemOutput->setText(COLUMN_DATE, QDateTime::fromTime_t(out.tx->GetTxTime()).toUTC().toString("yy-MM-dd hh:mm")); + + // immature PoS reward + { + // LOCK on cs_main must be taken for depth and maturity. + LOCK(cs_main); + + if (out.tx->IsCoinStake() && out.tx->GetBlocksToMaturity() > 0 && out.tx->GetDepthInMainChain() > 0) { + itemOutput->setBackground(COLUMN_CONFIRMATIONS, Qt::red); + itemOutput->setDisabled(true); + } + } + + // confirmations + itemOutput->setText(COLUMN_CONFIRMATIONS, strPad(QString::number(out.nDepth), 8, " ")); + + // priority + double dPriority = ((double)out.tx->vout[out.i].nValue / (nInputSize + 78)) * (out.nDepth+1); // 78 = 2 * 34 + 10 + itemOutput->setText(COLUMN_PRIORITY, CoinControlDialog::getPriorityLabel(dPriority)); + itemOutput->setText(COLUMN_PRIORITY_INT64, strPad(QString::number((int64_t)dPriority), 20, " ")); + dPrioritySum += (double)out.tx->vout[out.i].nValue * (out.nDepth+1); + nInputSum += nInputSize; + + // transaction hash + uint256 txhash = out.tx->GetHash(); + itemOutput->setText(COLUMN_TXHASH, txhash.GetHex().c_str()); + + // vout index + itemOutput->setText(COLUMN_VOUT_INDEX, QString::number(out.i)); + + // set checkbox + if (coinControl->IsSelected(txhash, out.i)) + { + itemOutput->setCheckState(COLUMN_CHECKBOX,Qt::Checked); + } + } + + // amount + if (treeMode) + { + dPrioritySum = dPrioritySum / (nInputSum + 78); + itemWalletAddress->setText(COLUMN_CHECKBOX, "(" + QString::number(nChildren) + ")"); + itemWalletAddress->setText(COLUMN_AMOUNT, BitcoinUnits::format(nDisplayUnit, nSum)); + itemWalletAddress->setText(COLUMN_AMOUNT_INT64, strPad(QString::number(nSum), 15, " ")); + itemWalletAddress->setText(COLUMN_PRIORITY, CoinControlDialog::getPriorityLabel(dPrioritySum)); + itemWalletAddress->setText(COLUMN_PRIORITY_INT64, strPad(QString::number((int64_t)dPrioritySum), 20, " ")); + } + } + + // 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); +} + +void ConsolidateUnspentWizardSelectInputsPage::SetOutputWarningStop(InputStatus input_status) +{ + switch (input_status) + { + case InputStatus::INSUFFICIENT_OUTPUTS: + ui->outputLimitWarningIconLabel->setVisible(false); + ui->outputLimitStopIconLabel->setVisible(false); + ui->isCompleteCheckBox->setChecked(false); + break; + case InputStatus::NORMAL: + ui->outputLimitWarningIconLabel->setVisible(false); + ui->outputLimitStopIconLabel->setVisible(false); + ui->isCompleteCheckBox->setChecked(true); + break; + case InputStatus::WARNING: + ui->outputLimitWarningIconLabel->setVisible(true); + ui->outputLimitStopIconLabel->setVisible(false); + ui->isCompleteCheckBox->setChecked(true); + break; + case InputStatus::STOP: + ui->outputLimitWarningIconLabel->setVisible(false); + ui->outputLimitStopIconLabel->setVisible(true); + ui->isCompleteCheckBox->setChecked(false); + } +} diff --git a/src/qt/consolidateunspentwizardselectinputspage.h b/src/qt/consolidateunspentwizardselectinputspage.h new file mode 100644 index 0000000000..5db61f2f5e --- /dev/null +++ b/src/qt/consolidateunspentwizardselectinputspage.h @@ -0,0 +1,91 @@ +#ifndef CONSOLIDATEUNSPENTWIZARDSELECTINPUTS_H +#define CONSOLIDATEUNSPENTWIZARDSELECTINPUTS_H + +#include "walletmodel.h" +#include "amount.h" + +#include +#include + +namespace Ui { + class ConsolidateUnspentWizardSelectInputsPage; +} + +class CoinControlDialog; + +class ConsolidateUnspentWizardSelectInputsPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit ConsolidateUnspentWizardSelectInputsPage(QWidget *parent = nullptr); + ~ConsolidateUnspentWizardSelectInputsPage(); + + void setCoinControl(CCoinControl* coinControl); + void setPayAmounts(QList *payAmounts); + +signals: + void setAddressListSignal(std::map); + void setDefaultAddressSignal(QString); + void updateFieldsSignal(); + +public slots: + void setModel(WalletModel*); + void updateLabels(); + +private: + Ui::ConsolidateUnspentWizardSelectInputsPage *ui; + CCoinControl *coinControl; + QList *payAmounts; + WalletModel *model; + int sortColumn; + Qt::SortOrder sortOrder; + size_t m_InputSelectionLimit; + Qt::CheckState m_ToState = Qt::Checked; + bool m_FilterMode = true; + bool m_FilterValueValid = false; + bool m_InputSelectionLimitedByFilter = false; + bool m_ViewItemsChangedViaFilter = false; + + QString strPad(QString, int, QString); + void sortView(int, Qt::SortOrder); + void updateView(); + bool filterInputsByValue(const bool& less, const CAmount& inputFilterValue, const unsigned int& inputSelectionLimit); + + enum + { + COLUMN_CHECKBOX, + COLUMN_AMOUNT, + COLUMN_LABEL, + COLUMN_ADDRESS, + COLUMN_DATE, + COLUMN_CONFIRMATIONS, + COLUMN_PRIORITY, + COLUMN_TXHASH, + COLUMN_VOUT_INDEX, + COLUMN_AMOUNT_INT64, + COLUMN_PRIORITY_INT64, + COLUMN_CHANGE_BOOL + }; + + enum InputStatus + { + INSUFFICIENT_OUTPUTS, + NORMAL, + WARNING, + STOP + }; + +private slots: + void treeModeRadioButton(bool); + void listModeRadioButton(bool); + void viewItemChanged(QTreeWidgetItem*, int); + void headerSectionClicked(int); + void buttonSelectAllClicked(); + void maxMinOutputValueChanged(); + void buttonFilterModeClicked(); + void buttonFilterClicked(); + void SetOutputWarningStop(InputStatus input_status); +}; + +#endif // CONSOLIDATEUNSPENTWIZARDSELECTINPUTS_H diff --git a/src/qt/consolidateunspentwizardsendpage.cpp b/src/qt/consolidateunspentwizardsendpage.cpp new file mode 100644 index 0000000000..2a50e3e080 --- /dev/null +++ b/src/qt/consolidateunspentwizardsendpage.cpp @@ -0,0 +1,53 @@ +#include "consolidateunspentwizardsendpage.h" +#include "ui_consolidateunspentwizardsendpage.h" + +#include "util.h" +#include "bitcoinunits.h" +#include "optionsmodel.h" + +ConsolidateUnspentWizardSendPage::ConsolidateUnspentWizardSendPage(QWidget *parent) : + QWizardPage(parent), + ui(new Ui::ConsolidateUnspentWizardSendPage) +{ + ui->setupUi(this); +} + +ConsolidateUnspentWizardSendPage::~ConsolidateUnspentWizardSendPage() +{ + delete ui; +} + +void ConsolidateUnspentWizardSendPage::initializePage() +{ + ui->InputQuantityLabel->setText(field("quantityField").toString()); + ui->feeLabel->setText(field("feeField").toString()); + ui->afterFeeAmountLabel->setText(field("afterFeeAmountField").toString()); + ui->destinationAddressLabelLabel->setText(field("selectedAddressLabelField").toString()); + ui->destinationAddressLabel->setText(field("selectedAddressField").toString()); + + LogPrint(BCLog::LogFlags::QT, "INFO: %s: destinationAddress = %s", + __func__, field("selectedAddressField").toString().toStdString()); + + qint64 amount = 0; + bool parse_status = false; + + m_recipient.label = ui->destinationAddressLabelLabel->text(); + m_recipient.address = ui->destinationAddressLabel->text(); + + parse_status = BitcoinUnits::parse(model->getOptionsModel()->getDisplayUnit(), + ui->afterFeeAmountLabel->text() + .left(ui->afterFeeAmountLabel->text().indexOf(" ")), + &amount); + + if (parse_status) m_recipient.amount = amount; +} + +void ConsolidateUnspentWizardSendPage::setModel(WalletModel *model) +{ + this->model = model; +} + +void ConsolidateUnspentWizardSendPage::onFinishButtonClicked() +{ + emit selectedConsolidationRecipientSignal(m_recipient); +} diff --git a/src/qt/consolidateunspentwizardsendpage.h b/src/qt/consolidateunspentwizardsendpage.h new file mode 100644 index 0000000000..974a1425ef --- /dev/null +++ b/src/qt/consolidateunspentwizardsendpage.h @@ -0,0 +1,38 @@ +#ifndef CONSOLIDATEUNSPENTWIZARDSENDPAGE_H +#define CONSOLIDATEUNSPENTWIZARDSENDPAGE_H + +#include "walletmodel.h" +#include "amount.h" + +#include + +namespace Ui { + class ConsolidateUnspentWizardSendPage; +} + +class ConsolidateUnspentWizardSendPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit ConsolidateUnspentWizardSendPage(QWidget *parent = nullptr); + ~ConsolidateUnspentWizardSendPage(); + + void initializePage(); + +public slots: + void setModel(WalletModel*); + void onFinishButtonClicked(); + +signals: + void selectedConsolidationRecipientSignal(SendCoinsRecipient consolidationRecipient); + +private: + Ui::ConsolidateUnspentWizardSendPage *ui; + WalletModel *model; + SendCoinsRecipient m_recipient; + + size_t m_inputSelectionLimit; +}; + +#endif // CONSOLIDATEUNSPENTWIZARDSENDPAGE_H diff --git a/src/qt/forms/consolidateunspentwizard.ui b/src/qt/forms/consolidateunspentwizard.ui new file mode 100644 index 0000000000..ddde9fb2d2 --- /dev/null +++ b/src/qt/forms/consolidateunspentwizard.ui @@ -0,0 +1,69 @@ + + + ConsolidateUnspentWizard + + + + 0 + 0 + 933 + 700 + + + + + 0 + 0 + + + + Conolidate Unspent Transaction Outputs (UTXOs) + + + true + + + true + + + QWizard::ClassicStyle + + + + 0 + + + + + 1 + + + + + 2 + + + + + + ConsolidateUnspentWizardSelectInputsPage + QWizardPage +
consolidateunspentwizardselectinputspage.h
+ 1 +
+ + ConsolidateUnspentWizardSelectDestinationPage + QWizardPage +
consolidateunspentwizardselectdestinationpage.h
+ 1 +
+ + ConsolidateUnspentWizardSendPage + QWizardPage +
consolidateunspentwizardsendpage.h
+ 1 +
+
+ + +
diff --git a/src/qt/forms/consolidateunspentwizardselectdestinationpage.ui b/src/qt/forms/consolidateunspentwizardselectdestinationpage.ui new file mode 100644 index 0000000000..c7486abf21 --- /dev/null +++ b/src/qt/forms/consolidateunspentwizardselectdestinationpage.ui @@ -0,0 +1,132 @@ + + + ConsolidateUnspentWizardSelectDestinationPage + + + + 0 + 0 + 900 + 700 + + + + WizardPage + + + + + 19 + 19 + 851 + 581 + + + + + + + Step 2: Select the destination address for the consolidation transaction. Note that all of the selected inputs will be consolidated to an output on this address. If there is a very small amount of change (due to uncertainty in the fee calculation), it will also be sent to this address. If you selected inputs only from a particular address on the previous page, then that address will already be selected by default. + + + true + + + + + + + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::ScrollPerPixel + + + 90 + + + true + + + false + + + + Label + + + + + Address + + + + + + + + + + + + Currently selected: + + + + + + + + + + + Label + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Address + + + + + + + + + isComplete + + + + + + + + + diff --git a/src/qt/forms/consolidateunspentwizardselectinputspage.ui b/src/qt/forms/consolidateunspentwizardselectinputspage.ui new file mode 100644 index 0000000000..bd719917df --- /dev/null +++ b/src/qt/forms/consolidateunspentwizardselectinputspage.ui @@ -0,0 +1,371 @@ + + + ConsolidateUnspentWizardSelectInputsPage + + + + 0 + 0 + 900 + 700 + + + + WizardPage + + + + + 20 + 20 + 851 + 581 + + + + + + + + + + 0 + 0 + + + + Step 1: Select the inputs to be consolidated. Remember that the inputs to the consolidation are your unspent outputs (UTXOs) in your wallet. + + + true + + + + + + + + + + + Select All + + + + + + + Tree Mode + + + true + + + + + + + List Mode + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Select inputs + + + + + + + + + + <= + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + Filters the already selected inputs. + + + Filter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::CustomContextMenu + + + false + + + 11 + + + true + + + false + + + + + + + + + Amount + + + + + Label + + + + + Address + + + + + Date + + + + + Confirmations + + + Confirmed + + + + + Priority + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + :/icons/warning + + + true + + + + + + + + 64 + 64 + + + + + + + :/icons/white_and_red_x + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Quantity + + + + + + + 99999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Fee + + + + + + + 99.9999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + After Fee Amount + + + + + + + 999999999.9999 + + + + + + + isComplete + + + + + + + + + + + CoinControlTreeWidget + QTreeWidget +
coincontroltreewidget.h
+
+ + BitcoinAmountField + QSpinBox +
bitcoinamountfield.h
+ 1 +
+
+ + + + +
diff --git a/src/qt/forms/consolidateunspentwizardsendpage.ui b/src/qt/forms/consolidateunspentwizardsendpage.ui new file mode 100644 index 0000000000..c0ce6e79b1 --- /dev/null +++ b/src/qt/forms/consolidateunspentwizardsendpage.ui @@ -0,0 +1,119 @@ + + + ConsolidateUnspentWizardSendPage + + + + 0 + 0 + 900 + 700 + + + + WizardPage + + + + + 19 + 19 + 851 + 581 + + + + + + + + + Step 3: Confirm Consolidation Transaction Details. Transaction will be ready to send when Finish is pressed. + + + true + + + + + + + + + + + Number of Inputs + + + + + + + 999999 + + + + + + + Transaction Fee + + + + + + + 99.9999 + + + + + + + Amount + + + + + + + 999999999.9999 + + + + + + + Destination Address + + + + + + + address + + + + + + + Destination Address Label + + + + + + + label + + + + + + + + + + + diff --git a/src/qt/forms/sendcoinsdialog.ui b/src/qt/forms/sendcoinsdialog.ui index 3478b8fb4f..17004d7df1 100644 --- a/src/qt/forms/sendcoinsdialog.ui +++ b/src/qt/forms/sendcoinsdialog.ui @@ -85,7 +85,7 @@ - + 8 @@ -129,6 +129,13 @@ + + + + Consolidate Wizard + + + diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 09c742b2b0..c8cbf75729 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -12,7 +12,10 @@ #include "askpassphrasedialog.h" #include "wallet/coincontrol.h" +#include "policy/policy.h" #include "coincontroldialog.h" +#include "consolidateunspentdialog.h" +#include "consolidateunspentwizard.h" #include #include @@ -23,6 +26,8 @@ SendCoinsDialog::SendCoinsDialog(QWidget *parent) : QDialog(parent), ui(new Ui::SendCoinsDialog), + coinControl(new CCoinControl), + payAmounts(new QList), model(0) { ui->setupUi(this); @@ -34,6 +39,7 @@ SendCoinsDialog::SendCoinsDialog(QWidget *parent) : // Coin Control ui->coinControlChangeEdit->setFont(GUIUtil::bitcoinAddressFont()); connect(ui->coinControlPushButton, SIGNAL(clicked()), this, SLOT(coinControlButtonClicked())); + connect(ui->coinControlConsolidateWizardPushButton, SIGNAL(clicked()), this, SLOT(coinControlConsolidateWizardButtonClicked())); connect(ui->coinControlResetPushButton, SIGNAL(clicked()), this, SLOT(coinControlResetButtonClicked())); connect(ui->coinControlChangeCheckBox, SIGNAL(stateChanged(int)), this, SLOT(coinControlChangeChecked(int))); connect(ui->coinControlChangeEdit, SIGNAL(textEdited(const QString &)), this, SLOT(coinControlChangeEdited(const QString &))); @@ -167,7 +173,7 @@ void SendCoinsDialog::on_sendButton_clicked() if (!model->getOptionsModel() || !model->getOptionsModel()->getCoinControlFeatures()) sendstatus = model->sendCoins(recipients); else - sendstatus = model->sendCoins(recipients, CoinControlDialog::coinControl); + sendstatus = model->sendCoins(recipients, coinControl); switch(sendstatus.status) { @@ -211,7 +217,7 @@ void SendCoinsDialog::on_sendButton_clicked() break; case WalletModel::OK: accept(); - CoinControlDialog::coinControl->UnSelectAll(); + coinControl->UnSelectAll(); coinControlUpdateLabels(); break; } @@ -423,13 +429,13 @@ void SendCoinsDialog::coinControlFeatureChanged(bool checked) ui->frameCoinControl->setVisible(checked); if (!checked && model) // coin control features disabled - CoinControlDialog::coinControl->SetNull(); + coinControl->SetNull(); } // Coin Control: button inputs -> show actual coin control dialog void SendCoinsDialog::coinControlButtonClicked() { - CoinControlDialog dlg; + CoinControlDialog dlg(this, coinControl, payAmounts); dlg.setModel(model); connect(&dlg, SIGNAL(selectedConsolidationRecipientSignal(SendCoinsRecipient)), @@ -441,12 +447,31 @@ void SendCoinsDialog::coinControlButtonClicked() void SendCoinsDialog::coinControlResetButtonClicked() { - CoinControlDialog::coinControl->SetNull(); + coinControl->SetNull(); coinControlUpdateLabels(); } +void SendCoinsDialog::coinControlConsolidateWizardButtonClicked() +{ + CoinControlDialog dlg(this, coinControl, payAmounts); + dlg.setModel(model); + + connect(&dlg, SIGNAL(selectedConsolidationRecipientSignal(SendCoinsRecipient)), + this, SLOT(selectedConsolidationRecipient(SendCoinsRecipient))); + + ConsolidateUnspentWizard wizard(this, coinControl, payAmounts, GetMaxInputsForConsolidationTxn()); + wizard.setModel(model); + + connect(&wizard, SIGNAL(selectedConsolidationRecipientSignal(SendCoinsRecipient)), + this, SLOT(selectedConsolidationRecipient(SendCoinsRecipient))); + + wizard.exec(); +} + void SendCoinsDialog::selectedConsolidationRecipient(SendCoinsRecipient consolidationRecipient) { + LogPrintf("INFO: %s: SLOT called.", __func__); + ui->coinControlChangeCheckBox->setChecked(true); ui->coinControlChangeEdit->setText(consolidationRecipient.address); @@ -469,9 +494,9 @@ void SendCoinsDialog::coinControlChangeChecked(int state) if (model) { if (state == Qt::Checked) - CoinControlDialog::coinControl->destChange = CBitcoinAddress(ui->coinControlChangeEdit->text().toStdString()).Get(); + coinControl->destChange = CBitcoinAddress(ui->coinControlChangeEdit->text().toStdString()).Get(); else - CoinControlDialog::coinControl->destChange = CNoDestination(); + coinControl->destChange = CNoDestination(); } ui->coinControlChangeEdit->setEnabled((state == Qt::Checked)); @@ -483,7 +508,7 @@ void SendCoinsDialog::coinControlChangeEdited(const QString & text) { if (model) { - CoinControlDialog::coinControl->destChange = CBitcoinAddress(text.toStdString()).Get(); + coinControl->destChange = CBitcoinAddress(text.toStdString()).Get(); // label for the change address ui->coinControlChangeLabel->setStyleSheet("QLabel{color:black;}"); @@ -523,18 +548,17 @@ void SendCoinsDialog::coinControlUpdateLabels() return; // set pay amounts - CoinControlDialog::payAmounts.clear(); - for(int i = 0; i < ui->entries->count(); ++i) + payAmounts->clear(); + for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry *entry = qobject_cast(ui->entries->itemAt(i)->widget()); - if(entry) - CoinControlDialog::payAmounts.append(entry->getValue().amount); + if (entry) payAmounts->append(entry->getValue().amount); } - if (CoinControlDialog::coinControl->HasSelected()) + if (coinControl->HasSelected()) { // actual coin control calculation - CoinControlDialog::updateLabels(model, this); + CoinControlDialog::updateLabels(model, coinControl, payAmounts, this); // show coin control stats ui->coinControlAutomaticallySelectedLabel->hide(); diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index 81878ca005..b0479bd9ce 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -43,6 +43,8 @@ public slots: private: Ui::SendCoinsDialog *ui; + CCoinControl *coinControl; + QList *payAmounts; WalletModel *model; bool fNewRecipientAllowed; @@ -53,6 +55,7 @@ private slots: void coinControlFeatureChanged(bool); void coinControlButtonClicked(); void coinControlResetButtonClicked(); + void coinControlConsolidateWizardButtonClicked(); void coinControlChangeChecked(int); void coinControlChangeEdited(const QString &); void coinControlUpdateLabels();