eec7757445
Introduce a PlatformStyle to handle platform-specific customization of the UI. This replaces 'scicon', as well as #ifdefs to determine whether to place icons on buttons. The selected PlatformStyle defaults to the platform that the application was compiled on, but can be overridden from the command line with `-uiplatform=<x>`. Also fixes the warning from #6328.
827 lines
34 KiB
C++
827 lines
34 KiB
C++
// Copyright (c) 2011-2013 The Bitcoin Core developers
|
|
// Distributed under the MIT software license, see the accompanying
|
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
#include "coincontroldialog.h"
|
|
#include "ui_coincontroldialog.h"
|
|
|
|
#include "addresstablemodel.h"
|
|
#include "bitcoinunits.h"
|
|
#include "guiutil.h"
|
|
#include "optionsmodel.h"
|
|
#include "platformstyle.h"
|
|
#include "txmempool.h"
|
|
#include "walletmodel.h"
|
|
|
|
#include "coincontrol.h"
|
|
#include "init.h"
|
|
#include "main.h" // For minRelayTxFee
|
|
#include "wallet/wallet.h"
|
|
|
|
#include <boost/assign/list_of.hpp> // for 'map_list_of()'
|
|
|
|
#include <QApplication>
|
|
#include <QCheckBox>
|
|
#include <QCursor>
|
|
#include <QDialogButtonBox>
|
|
#include <QFlags>
|
|
#include <QIcon>
|
|
#include <QSettings>
|
|
#include <QString>
|
|
#include <QTreeWidget>
|
|
#include <QTreeWidgetItem>
|
|
|
|
QList<CAmount> CoinControlDialog::payAmounts;
|
|
CCoinControl* CoinControlDialog::coinControl = new CCoinControl();
|
|
bool CoinControlDialog::fSubtractFeeFromAmount = false;
|
|
|
|
CoinControlDialog::CoinControlDialog(const PlatformStyle *platformStyle, QWidget *parent) :
|
|
QDialog(parent),
|
|
ui(new Ui::CoinControlDialog),
|
|
model(0),
|
|
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);
|
|
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); // we need to enable/disable this
|
|
|
|
// context menu
|
|
contextMenu = new QMenu();
|
|
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, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showMenu(QPoint)));
|
|
connect(copyAddressAction, SIGNAL(triggered()), this, SLOT(copyAddress()));
|
|
connect(copyLabelAction, SIGNAL(triggered()), this, SLOT(copyLabel()));
|
|
connect(copyAmountAction, SIGNAL(triggered()), this, SLOT(copyAmount()));
|
|
connect(copyTransactionHashAction, SIGNAL(triggered()), this, SLOT(copyTransactionHash()));
|
|
connect(lockAction, SIGNAL(triggered()), this, SLOT(lockCoin()));
|
|
connect(unlockAction, SIGNAL(triggered()), this, SLOT(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 *clipboardPriorityAction = new QAction(tr("Copy priority"), this);
|
|
QAction *clipboardLowOutputAction = new QAction(tr("Copy dust"), this);
|
|
QAction *clipboardChangeAction = new QAction(tr("Copy change"), this);
|
|
|
|
connect(clipboardQuantityAction, SIGNAL(triggered()), this, SLOT(clipboardQuantity()));
|
|
connect(clipboardAmountAction, SIGNAL(triggered()), this, SLOT(clipboardAmount()));
|
|
connect(clipboardFeeAction, SIGNAL(triggered()), this, SLOT(clipboardFee()));
|
|
connect(clipboardAfterFeeAction, SIGNAL(triggered()), this, SLOT(clipboardAfterFee()));
|
|
connect(clipboardBytesAction, SIGNAL(triggered()), this, SLOT(clipboardBytes()));
|
|
connect(clipboardPriorityAction, SIGNAL(triggered()), this, SLOT(clipboardPriority()));
|
|
connect(clipboardLowOutputAction, SIGNAL(triggered()), this, SLOT(clipboardLowOutput()));
|
|
connect(clipboardChangeAction, SIGNAL(triggered()), this, SLOT(clipboardChange()));
|
|
|
|
ui->labelCoinControlQuantity->addAction(clipboardQuantityAction);
|
|
ui->labelCoinControlAmount->addAction(clipboardAmountAction);
|
|
ui->labelCoinControlFee->addAction(clipboardFeeAction);
|
|
ui->labelCoinControlAfterFee->addAction(clipboardAfterFeeAction);
|
|
ui->labelCoinControlBytes->addAction(clipboardBytesAction);
|
|
ui->labelCoinControlPriority->addAction(clipboardPriorityAction);
|
|
ui->labelCoinControlLowOutput->addAction(clipboardLowOutputAction);
|
|
ui->labelCoinControlChange->addAction(clipboardChangeAction);
|
|
|
|
// toggle tree/list mode
|
|
connect(ui->radioTreeMode, SIGNAL(toggled(bool)), this, SLOT(radioTreeMode(bool)));
|
|
connect(ui->radioListMode, SIGNAL(toggled(bool)), this, SLOT(radioListMode(bool)));
|
|
|
|
// click on checkbox
|
|
connect(ui->treeWidget, SIGNAL(itemChanged(QTreeWidgetItem*, int)), this, SLOT(viewItemChanged(QTreeWidgetItem*, int)));
|
|
|
|
// click on header
|
|
#if QT_VERSION < 0x050000
|
|
ui->treeWidget->header()->setClickable(true);
|
|
#else
|
|
ui->treeWidget->header()->setSectionsClickable(true);
|
|
#endif
|
|
connect(ui->treeWidget->header(), SIGNAL(sectionClicked(int)), this, SLOT(headerSectionClicked(int)));
|
|
|
|
// ok button
|
|
connect(ui->buttonBox, SIGNAL(clicked( QAbstractButton*)), this, SLOT(buttonBoxClicked(QAbstractButton*)));
|
|
|
|
// (un)select all
|
|
connect(ui->pushButtonSelectAll, SIGNAL(clicked()), this, SLOT(buttonSelectAllClicked()));
|
|
|
|
// change coin control first column label due Qt4 bug.
|
|
// see https://github.com/bitcoin/bitcoin/issues/5716
|
|
ui->treeWidget->headerItem()->setText(COLUMN_CHECKBOX, QString());
|
|
|
|
ui->treeWidget->setColumnWidth(COLUMN_CHECKBOX, 84);
|
|
ui->treeWidget->setColumnWidth(COLUMN_AMOUNT, 100);
|
|
ui->treeWidget->setColumnWidth(COLUMN_LABEL, 170);
|
|
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 in this column, but don't show it
|
|
ui->treeWidget->setColumnHidden(COLUMN_PRIORITY_INT64, true); // store priority int64 in this column, but don't show it
|
|
ui->treeWidget->setColumnHidden(COLUMN_DATE_INT64, true); // store date int64 in this column, but don't show it
|
|
|
|
// default view is sorted by amount desc
|
|
sortView(COLUMN_AMOUNT_INT64, 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(), ((Qt::SortOrder)settings.value("nCoinControlSortOrder").toInt()));
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// helper function str_pad
|
|
QString CoinControlDialog::strPad(QString s, int nPadLength, QString sPadding)
|
|
{
|
|
while (s.length() < nPadLength)
|
|
s = sPadding + s;
|
|
|
|
return s;
|
|
}
|
|
|
|
// ok button
|
|
void CoinControlDialog::buttonBoxClicked(QAbstractButton* button)
|
|
{
|
|
if (ui->buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole)
|
|
done(QDialog::Accepted); // closes the dialog
|
|
}
|
|
|
|
// (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)
|
|
coinControl->UnSelectAll(); // just to be sure
|
|
CoinControlDialog::updateLabels(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->text(COLUMN_TXHASH).length() == 64) // transaction hash is 64 characters (this means its a child node, so its not a parent node in tree mode)
|
|
{
|
|
copyTransactionHashAction->setEnabled(true);
|
|
if (model->isLockedCoin(uint256S(item->text(COLUMN_TXHASH).toStdString()), item->text(COLUMN_VOUT_INDEX).toUInt()))
|
|
{
|
|
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->text(COLUMN_TXHASH));
|
|
}
|
|
|
|
// context menu action: lock coin
|
|
void CoinControlDialog::lockCoin()
|
|
{
|
|
if (contextMenuItem->checkState(COLUMN_CHECKBOX) == Qt::Checked)
|
|
contextMenuItem->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked);
|
|
|
|
COutPoint outpt(uint256S(contextMenuItem->text(COLUMN_TXHASH).toStdString()), contextMenuItem->text(COLUMN_VOUT_INDEX).toUInt());
|
|
model->lockCoin(outpt);
|
|
contextMenuItem->setDisabled(true);
|
|
contextMenuItem->setIcon(COLUMN_CHECKBOX, platformStyle->SingleColorIcon(":/icons/lock_closed"));
|
|
updateLabelLocked();
|
|
}
|
|
|
|
// context menu action: unlock coin
|
|
void CoinControlDialog::unlockCoin()
|
|
{
|
|
COutPoint outpt(uint256S(contextMenuItem->text(COLUMN_TXHASH).toStdString()), contextMenuItem->text(COLUMN_VOUT_INDEX).toUInt());
|
|
model->unlockCoin(outpt);
|
|
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 "Priority" to clipboard
|
|
void CoinControlDialog::clipboardPriority()
|
|
{
|
|
GUIUtil::setClipboard(ui->labelCoinControlPriority->text());
|
|
}
|
|
|
|
// 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(getMappedColumn(sortColumn), sortOrder);
|
|
}
|
|
|
|
// treeview: clicked on header
|
|
void CoinControlDialog::headerSectionClicked(int logicalIndex)
|
|
{
|
|
if (logicalIndex == COLUMN_CHECKBOX) // click on most left column -> do nothing
|
|
{
|
|
ui->treeWidget->header()->setSortIndicator(getMappedColumn(sortColumn), sortOrder);
|
|
}
|
|
else
|
|
{
|
|
logicalIndex = getMappedColumn(logicalIndex, false);
|
|
|
|
if (sortColumn == logicalIndex)
|
|
sortOrder = ((sortOrder == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder);
|
|
else
|
|
{
|
|
sortColumn = logicalIndex;
|
|
sortOrder = ((sortColumn == COLUMN_LABEL || sortColumn == COLUMN_ADDRESS) ? Qt::AscendingOrder : Qt::DescendingOrder); // if label or address then default => asc, else default => desc
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (column == COLUMN_CHECKBOX && item->text(COLUMN_TXHASH).length() == 64) // transaction hash is 64 characters (this means its a child node, so its not a parent node in tree mode)
|
|
{
|
|
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
|
|
CoinControlDialog::updateLabels(model, this);
|
|
}
|
|
|
|
// todo: this is a temporary qt5 fix: when clicking a parent node in tree mode, the parent node
|
|
// including all children are partially selected. But the parent node should be fully selected
|
|
// as well as the children. Children should never be partially selected in the first place.
|
|
// Please remove this ugly fix, once the bug is solved upstream.
|
|
#if QT_VERSION >= 0x050000
|
|
else if (column == COLUMN_CHECKBOX && item->childCount() > 0)
|
|
{
|
|
if (item->checkState(COLUMN_CHECKBOX) == Qt::PartiallyChecked && item->child(0)->checkState(COLUMN_CHECKBOX) == Qt::PartiallyChecked)
|
|
item->setCheckState(COLUMN_CHECKBOX, Qt::Checked);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// return human readable label for priority number
|
|
QString CoinControlDialog::getPriorityLabel(double dPriority, double mempoolEstimatePriority)
|
|
{
|
|
double dPriorityMedium = mempoolEstimatePriority;
|
|
|
|
if (dPriorityMedium <= 0)
|
|
dPriorityMedium = AllowFreeThreshold(); // not enough data, back to hard-coded
|
|
|
|
if (dPriority / 1000000 > dPriorityMedium) return tr("highest");
|
|
else if (dPriority / 100000 > dPriorityMedium) return tr("higher");
|
|
else if (dPriority / 10000 > dPriorityMedium) return tr("high");
|
|
else if (dPriority / 1000 > dPriorityMedium) return tr("medium-high");
|
|
else if (dPriority > dPriorityMedium) return tr("medium");
|
|
else if (dPriority * 10 > dPriorityMedium) return tr("low-medium");
|
|
else if (dPriority * 100 > dPriorityMedium) return tr("low");
|
|
else if (dPriority * 1000 > dPriorityMedium) return tr("lower");
|
|
else return tr("lowest");
|
|
}
|
|
|
|
// shows count of locked unspent outputs
|
|
void CoinControlDialog::updateLabelLocked()
|
|
{
|
|
std::vector<COutPoint> vOutpts;
|
|
model->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)
|
|
{
|
|
if (!model)
|
|
return;
|
|
|
|
// nPayAmount
|
|
CAmount nPayAmount = 0;
|
|
bool fDust = false;
|
|
CMutableTransaction txDummy;
|
|
Q_FOREACH(const CAmount &amount, CoinControlDialog::payAmounts)
|
|
{
|
|
nPayAmount += amount;
|
|
|
|
if (amount > 0)
|
|
{
|
|
CTxOut txout(amount, (CScript)std::vector<unsigned char>(24, 0));
|
|
txDummy.vout.push_back(txout);
|
|
if (txout.IsDust(::minRelayTxFee))
|
|
fDust = true;
|
|
}
|
|
}
|
|
|
|
QString sPriorityLabel = tr("none");
|
|
CAmount nAmount = 0;
|
|
CAmount nPayFee = 0;
|
|
CAmount nAfterFee = 0;
|
|
CAmount nChange = 0;
|
|
unsigned int nBytes = 0;
|
|
unsigned int nBytesInputs = 0;
|
|
double dPriority = 0;
|
|
double dPriorityInputs = 0;
|
|
unsigned int nQuantity = 0;
|
|
int nQuantityUncompressed = 0;
|
|
bool fAllowFree = false;
|
|
|
|
std::vector<COutPoint> vCoinControl;
|
|
std::vector<COutput> vOutputs;
|
|
coinControl->ListSelected(vCoinControl);
|
|
model->getOutputs(vCoinControl, vOutputs);
|
|
|
|
BOOST_FOREACH(const COutput& out, vOutputs) {
|
|
// unselect already spent, very unlikely scenario, this could happen
|
|
// when selected are spent elsewhere, like rpc or another computer
|
|
uint256 txhash = out.tx->GetHash();
|
|
COutPoint outpt(txhash, out.i);
|
|
if (model->isSpent(outpt))
|
|
{
|
|
coinControl->UnSelect(outpt);
|
|
continue;
|
|
}
|
|
|
|
// Quantity
|
|
nQuantity++;
|
|
|
|
// Amount
|
|
nAmount += out.tx->vout[out.i].nValue;
|
|
|
|
// Priority
|
|
dPriorityInputs += (double)out.tx->vout[out.i].nValue * (out.nDepth+1);
|
|
|
|
// Bytes
|
|
CTxDestination address;
|
|
if(ExtractDestination(out.tx->vout[out.i].scriptPubKey, address))
|
|
{
|
|
CPubKey pubkey;
|
|
CKeyID *keyid = boost::get<CKeyID>(&address);
|
|
if (keyid && model->getPubKey(*keyid, pubkey))
|
|
{
|
|
nBytesInputs += (pubkey.IsCompressed() ? 148 : 180);
|
|
if (!pubkey.IsCompressed())
|
|
nQuantityUncompressed++;
|
|
}
|
|
else
|
|
nBytesInputs += 148; // in all error cases, simply assume 148 here
|
|
}
|
|
else nBytesInputs += 148;
|
|
}
|
|
|
|
// calculation
|
|
if (nQuantity > 0)
|
|
{
|
|
// Bytes
|
|
nBytes = nBytesInputs + ((CoinControlDialog::payAmounts.size() > 0 ? CoinControlDialog::payAmounts.size() + 1 : 2) * 34) + 10; // always assume +1 output for change here
|
|
|
|
// Priority
|
|
double mempoolEstimatePriority = mempool.estimatePriority(nTxConfirmTarget);
|
|
dPriority = dPriorityInputs / (nBytes - nBytesInputs + (nQuantityUncompressed * 29)); // 29 = 180 - 151 (uncompressed public keys are over the limit. max 151 bytes of the input are ignored for priority)
|
|
sPriorityLabel = CoinControlDialog::getPriorityLabel(dPriority, mempoolEstimatePriority);
|
|
|
|
// 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 == 0)
|
|
nBytes -= 34;
|
|
|
|
// Fee
|
|
nPayFee = CWallet::GetMinimumFee(nBytes, nTxConfirmTarget, mempool);
|
|
|
|
// Allow free?
|
|
double dPriorityNeeded = mempoolEstimatePriority;
|
|
if (dPriorityNeeded <= 0)
|
|
dPriorityNeeded = AllowFreeThreshold(); // not enough data, back to hard-coded
|
|
fAllowFree = (dPriority >= dPriorityNeeded);
|
|
|
|
if (fSendFreeTransactions)
|
|
if (fAllowFree && nBytes <= MAX_FREE_TRANSACTION_CREATE_SIZE)
|
|
nPayFee = 0;
|
|
|
|
if (nPayAmount > 0)
|
|
{
|
|
nChange = nAmount - nPayAmount;
|
|
if (!CoinControlDialog::fSubtractFeeFromAmount)
|
|
nChange -= nPayFee;
|
|
|
|
// Never create dust outputs; if we would, just add the dust to the fee.
|
|
if (nChange > 0 && nChange < CENT)
|
|
{
|
|
CTxOut txout(nChange, (CScript)std::vector<unsigned char>(24, 0));
|
|
if (txout.IsDust(::minRelayTxFee))
|
|
{
|
|
if (CoinControlDialog::fSubtractFeeFromAmount) // dust-change will be raised until no dust
|
|
nChange = txout.GetDustThreshold(::minRelayTxFee);
|
|
else
|
|
{
|
|
nPayFee += nChange;
|
|
nChange = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nChange == 0 && !CoinControlDialog::fSubtractFeeFromAmount)
|
|
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();
|
|
|
|
QLabel *l1 = dialog->findChild<QLabel *>("labelCoinControlQuantity");
|
|
QLabel *l2 = dialog->findChild<QLabel *>("labelCoinControlAmount");
|
|
QLabel *l3 = dialog->findChild<QLabel *>("labelCoinControlFee");
|
|
QLabel *l4 = dialog->findChild<QLabel *>("labelCoinControlAfterFee");
|
|
QLabel *l5 = dialog->findChild<QLabel *>("labelCoinControlBytes");
|
|
QLabel *l6 = dialog->findChild<QLabel *>("labelCoinControlPriority");
|
|
QLabel *l7 = dialog->findChild<QLabel *>("labelCoinControlLowOutput");
|
|
QLabel *l8 = dialog->findChild<QLabel *>("labelCoinControlChange");
|
|
|
|
// enable/disable "dust" and "change"
|
|
dialog->findChild<QLabel *>("labelCoinControlLowOutputText")->setEnabled(nPayAmount > 0);
|
|
dialog->findChild<QLabel *>("labelCoinControlLowOutput") ->setEnabled(nPayAmount > 0);
|
|
dialog->findChild<QLabel *>("labelCoinControlChangeText") ->setEnabled(nPayAmount > 0);
|
|
dialog->findChild<QLabel *>("labelCoinControlChange") ->setEnabled(nPayAmount > 0);
|
|
|
|
// stats
|
|
l1->setText(QString::number(nQuantity)); // Quantity
|
|
l2->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAmount)); // Amount
|
|
l3->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nPayFee)); // Fee
|
|
l4->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nAfterFee)); // After Fee
|
|
l5->setText(((nBytes > 0) ? ASYMP_UTF8 : "") + QString::number(nBytes)); // Bytes
|
|
l6->setText(sPriorityLabel); // Priority
|
|
l7->setText(fDust ? tr("yes") : tr("no")); // Dust
|
|
l8->setText(BitcoinUnits::formatWithUnit(nDisplayUnit, nChange)); // Change
|
|
if (nPayFee > 0 && !(payTxFee.GetFeePerK() > 0 && fPayAtLeastCustomFee && nBytes < 1000))
|
|
{
|
|
l3->setText(ASYMP_UTF8 + l3->text());
|
|
l4->setText(ASYMP_UTF8 + l4->text());
|
|
if (nChange > 0 && !CoinControlDialog::fSubtractFeeFromAmount)
|
|
l8->setText(ASYMP_UTF8 + l8->text());
|
|
}
|
|
|
|
// turn labels "red"
|
|
l5->setStyleSheet((nBytes >= MAX_FREE_TRANSACTION_CREATE_SIZE) ? "color:red;" : "");// Bytes >= 1000
|
|
l6->setStyleSheet((dPriority > 0 && !fAllowFree) ? "color:red;" : ""); // Priority < "medium"
|
|
l7->setStyleSheet((fDust) ? "color:red;" : ""); // Dust = "yes"
|
|
|
|
// tool tips
|
|
QString toolTip1 = tr("This label turns red if the transaction size is greater than 1000 bytes.") + "<br /><br />";
|
|
toolTip1 += tr("This means a fee of at least %1 per kB is required.").arg(BitcoinUnits::formatWithUnit(nDisplayUnit, CWallet::minTxFee.GetFeePerK())) + "<br /><br />";
|
|
toolTip1 += tr("Can vary +/- 1 byte per input.");
|
|
|
|
QString toolTip2 = tr("Transactions with higher priority are more likely to get included into a block.") + "<br /><br />";
|
|
toolTip2 += tr("This label turns red if the priority is smaller than \"medium\".") + "<br /><br />";
|
|
toolTip2 += tr("This means a fee of at least %1 per kB is required.").arg(BitcoinUnits::formatWithUnit(nDisplayUnit, CWallet::minTxFee.GetFeePerK()));
|
|
|
|
QString toolTip3 = tr("This label turns red if any recipient receives an amount smaller than %1.").arg(BitcoinUnits::formatWithUnit(nDisplayUnit, ::minRelayTxFee.GetFee(546)));
|
|
|
|
// how many satoshis the estimated fee can vary per byte we guess wrong
|
|
double dFeeVary;
|
|
if (payTxFee.GetFeePerK() > 0)
|
|
dFeeVary = (double)std::max(CWallet::minTxFee.GetFeePerK(), payTxFee.GetFeePerK()) / 1000;
|
|
else
|
|
dFeeVary = (double)std::max(CWallet::minTxFee.GetFeePerK(), mempool.estimateFee(nTxConfirmTarget).GetFeePerK()) / 1000;
|
|
QString toolTip4 = tr("Can vary +/- %1 satoshi(s) per input.").arg(dFeeVary);
|
|
|
|
l3->setToolTip(toolTip4);
|
|
l4->setToolTip(toolTip4);
|
|
l5->setToolTip(toolTip1);
|
|
l6->setToolTip(toolTip2);
|
|
l7->setToolTip(toolTip3);
|
|
l8->setToolTip(toolTip4);
|
|
dialog->findChild<QLabel *>("labelCoinControlFeeText") ->setToolTip(l3->toolTip());
|
|
dialog->findChild<QLabel *>("labelCoinControlAfterFeeText") ->setToolTip(l4->toolTip());
|
|
dialog->findChild<QLabel *>("labelCoinControlBytesText") ->setToolTip(l5->toolTip());
|
|
dialog->findChild<QLabel *>("labelCoinControlPriorityText") ->setToolTip(l6->toolTip());
|
|
dialog->findChild<QLabel *>("labelCoinControlLowOutputText")->setToolTip(l7->toolTip());
|
|
dialog->findChild<QLabel *>("labelCoinControlChangeText") ->setToolTip(l8->toolTip());
|
|
|
|
// Insufficient funds
|
|
QLabel *label = dialog->findChild<QLabel *>("labelCoinControlInsuffFunds");
|
|
if (label)
|
|
label->setVisible(nChange < 0);
|
|
}
|
|
|
|
void CoinControlDialog::updateView()
|
|
{
|
|
if (!model || !model->getOptionsModel() || !model->getAddressTableModel())
|
|
return;
|
|
|
|
bool treeMode = ui->radioTreeMode->isChecked();
|
|
|
|
ui->treeWidget->clear();
|
|
ui->treeWidget->setEnabled(false); // performance, otherwise updateLabels would be called for every checked checkbox
|
|
ui->treeWidget->setAlternatingRowColors(!treeMode);
|
|
QFlags<Qt::ItemFlag> flgCheckbox = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable;
|
|
QFlags<Qt::ItemFlag> flgTristate = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable | Qt::ItemIsTristate;
|
|
|
|
int nDisplayUnit = model->getOptionsModel()->getDisplayUnit();
|
|
double mempoolEstimatePriority = mempool.estimatePriority(nTxConfirmTarget);
|
|
|
|
std::map<QString, std::vector<COutput> > mapCoins;
|
|
model->listCoins(mapCoins);
|
|
|
|
BOOST_FOREACH(const PAIRTYPE(QString, std::vector<COutput>)& coins, mapCoins) {
|
|
QTreeWidgetItem *itemWalletAddress = new QTreeWidgetItem();
|
|
itemWalletAddress->setCheckState(COLUMN_CHECKBOX, Qt::Unchecked);
|
|
QString sWalletAddress = coins.first;
|
|
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);
|
|
}
|
|
|
|
CAmount nSum = 0;
|
|
double dPrioritySum = 0;
|
|
int nChildren = 0;
|
|
int nInputSum = 0;
|
|
BOOST_FOREACH(const COutput& out, coins.second) {
|
|
int nInputSize = 0;
|
|
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 = QString::fromStdString(CBitcoinAddress(outputAddress).ToString());
|
|
|
|
// 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;
|
|
CKeyID *keyid = boost::get<CKeyID>(&outputAddress);
|
|
if (keyid && model->getPubKey(*keyid, pubkey) && !pubkey.IsCompressed())
|
|
nInputSize = 29; // 29 = 180 - 151 (public key is 180 bytes, priority free area is 151 bytes)
|
|
}
|
|
|
|
// 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.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, GUIUtil::dateTimeStr(out.tx->GetTxTime()));
|
|
itemOutput->setText(COLUMN_DATE_INT64, strPad(QString::number(out.tx->GetTxTime()), 20, " "));
|
|
|
|
// 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, mempoolEstimatePriority));
|
|
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, QString::fromStdString(txhash.GetHex()));
|
|
|
|
// vout index
|
|
itemOutput->setText(COLUMN_VOUT_INDEX, QString::number(out.i));
|
|
|
|
// disable locked coins
|
|
if (model->isLockedCoin(txhash, out.i))
|
|
{
|
|
COutPoint outpt(txhash, out.i);
|
|
coinControl->UnSelect(outpt); // just to be sure
|
|
itemOutput->setDisabled(true);
|
|
itemOutput->setIcon(COLUMN_CHECKBOX, platformStyle->SingleColorIcon(":/icons/lock_closed"));
|
|
}
|
|
|
|
// 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, mempoolEstimatePriority));
|
|
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);
|
|
}
|