2020-10-07 10:36:04 +00:00
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2020-12-26 19:56:06 +00:00
|
|
|
// Copyright (c) 2020-2021, The Monero Project.
|
2020-10-07 10:36:04 +00:00
|
|
|
|
2021-06-28 17:48:23 +00:00
|
|
|
#include "CoinsWidget.h"
|
|
|
|
#include "ui_CoinsWidget.h"
|
|
|
|
|
|
|
|
#include <QMessageBox>
|
|
|
|
|
2021-06-27 11:46:32 +00:00
|
|
|
#include "dialog/OutputInfoDialog.h"
|
|
|
|
#include "dialog/OutputSweepDialog.h"
|
2021-05-02 18:22:38 +00:00
|
|
|
#include "utils/Icons.h"
|
2020-10-07 10:36:04 +00:00
|
|
|
|
2021-05-18 15:59:18 +00:00
|
|
|
CoinsWidget::CoinsWidget(QSharedPointer<AppContext> ctx, QWidget *parent)
|
2021-06-28 17:48:23 +00:00
|
|
|
: QWidget(parent)
|
|
|
|
, ui(new Ui::CoinsWidget)
|
|
|
|
, m_ctx(std::move(ctx))
|
|
|
|
, m_headerMenu(new QMenu(this))
|
|
|
|
, m_copyMenu(new QMenu("Copy",this))
|
2020-10-07 10:36:04 +00:00
|
|
|
{
|
|
|
|
ui->setupUi(this);
|
|
|
|
|
|
|
|
// header context menu
|
|
|
|
ui->coins->header()->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
m_showSpentAction = m_headerMenu->addAction("Show spent outputs", this, &CoinsWidget::setShowSpent);
|
|
|
|
m_showSpentAction->setCheckable(true);
|
|
|
|
connect(ui->coins->header(), &QHeaderView::customContextMenuRequested, this, &CoinsWidget::showHeaderMenu);
|
|
|
|
|
|
|
|
// copy menu
|
2021-05-02 18:22:38 +00:00
|
|
|
m_copyMenu->setIcon(icons()->icon("copy.png"));
|
2020-10-07 10:36:04 +00:00
|
|
|
m_copyMenu->addAction("Public key", this, [this]{copy(copyField::PubKey);});
|
|
|
|
m_copyMenu->addAction("Key Image", this, [this]{copy(copyField::KeyImage);});
|
|
|
|
m_copyMenu->addAction("Transaction ID", this, [this]{copy(copyField::TxID);});
|
|
|
|
m_copyMenu->addAction("Address", this, [this]{copy(copyField::Address);});
|
2020-12-14 01:30:30 +00:00
|
|
|
m_copyMenu->addAction("Label", this, [this]{copy(copyField::Label);});
|
2020-10-07 10:36:04 +00:00
|
|
|
m_copyMenu->addAction("Height", this, [this]{copy(copyField::Height);});
|
|
|
|
m_copyMenu->addAction("Amount", this, [this]{copy(copyField::Amount);});
|
|
|
|
|
|
|
|
// context menu
|
|
|
|
ui->coins->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
|
2020-10-21 14:53:17 +00:00
|
|
|
m_thawOutputAction = new QAction("Thaw output", this);
|
|
|
|
m_freezeOutputAction = new QAction("Freeze output", this);
|
2020-10-07 10:36:04 +00:00
|
|
|
|
2020-10-21 14:53:17 +00:00
|
|
|
m_freezeAllSelectedAction = new QAction("Freeze selected", this);
|
|
|
|
m_thawAllSelectedAction = new QAction("Thaw selected", this);
|
2020-10-07 10:36:04 +00:00
|
|
|
|
2021-05-02 18:22:38 +00:00
|
|
|
m_viewOutputAction = new QAction(icons()->icon("info2.svg"), "Details", this);
|
2020-10-21 14:53:17 +00:00
|
|
|
m_sweepOutputAction = new QAction("Sweep output", this);
|
2021-06-28 16:05:21 +00:00
|
|
|
m_sweepOutputsAction = new QAction("Sweep selected outputs", this);
|
2020-10-07 10:36:04 +00:00
|
|
|
connect(m_freezeOutputAction, &QAction::triggered, this, &CoinsWidget::freezeOutput);
|
|
|
|
connect(m_thawOutputAction, &QAction::triggered, this, &CoinsWidget::thawOutput);
|
|
|
|
connect(m_viewOutputAction, &QAction::triggered, this, &CoinsWidget::viewOutput);
|
|
|
|
connect(m_sweepOutputAction, &QAction::triggered, this, &CoinsWidget::onSweepOutput);
|
2021-06-28 16:05:21 +00:00
|
|
|
connect(m_sweepOutputsAction, &QAction::triggered, this, &CoinsWidget::onSweepMulti);
|
2020-10-07 10:36:04 +00:00
|
|
|
|
|
|
|
connect(m_freezeAllSelectedAction, &QAction::triggered, this, &CoinsWidget::freezeAllSelected);
|
|
|
|
connect(m_thawAllSelectedAction, &QAction::triggered, this, &CoinsWidget::thawAllSelected);
|
|
|
|
|
|
|
|
connect(ui->coins, &QTreeView::customContextMenuRequested, this, &CoinsWidget::showContextMenu);
|
2020-10-11 19:45:28 +00:00
|
|
|
connect(ui->coins, &QTreeView::doubleClicked, this, &CoinsWidget::viewOutput);
|
2021-06-28 17:20:16 +00:00
|
|
|
|
|
|
|
connect(ui->search, &QLineEdit::textChanged, this, &CoinsWidget::setSearchFilter);
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CoinsWidget::setModel(CoinsModel * model, Coins * coins) {
|
|
|
|
m_coins = coins;
|
|
|
|
m_model = model;
|
2021-02-03 23:14:20 +00:00
|
|
|
m_proxyModel = new CoinsProxyModel(this, m_coins);
|
2020-10-07 10:36:04 +00:00
|
|
|
m_proxyModel->setSourceModel(m_model);
|
|
|
|
ui->coins->setModel(m_proxyModel);
|
|
|
|
ui->coins->setColumnHidden(CoinsModel::Spent, true);
|
|
|
|
ui->coins->setColumnHidden(CoinsModel::SpentHeight, true);
|
|
|
|
ui->coins->setColumnHidden(CoinsModel::Frozen, true);
|
|
|
|
|
2021-05-18 15:59:18 +00:00
|
|
|
if (!m_ctx->wallet->viewOnly()) {
|
2020-10-14 02:21:38 +00:00
|
|
|
ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, true);
|
|
|
|
} else {
|
|
|
|
ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, false);
|
|
|
|
}
|
|
|
|
|
2020-10-07 10:36:04 +00:00
|
|
|
ui->coins->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
|
|
|
ui->coins->header()->setSectionResizeMode(CoinsModel::AddressLabel, QHeaderView::Stretch);
|
|
|
|
ui->coins->header()->setSortIndicator(CoinsModel::BlockHeight, Qt::DescendingOrder);
|
|
|
|
ui->coins->setSortingEnabled(true);
|
|
|
|
}
|
|
|
|
|
2021-06-28 17:20:16 +00:00
|
|
|
void CoinsWidget::setSearchbarVisible(bool visible) {
|
|
|
|
ui->search->setVisible(visible);
|
|
|
|
}
|
|
|
|
|
2020-10-07 10:36:04 +00:00
|
|
|
void CoinsWidget::showContextMenu(const QPoint &point) {
|
|
|
|
QModelIndexList list = ui->coins->selectionModel()->selectedRows();
|
|
|
|
|
|
|
|
auto *menu = new QMenu(ui->coins);
|
|
|
|
if (list.size() > 1) {
|
|
|
|
menu->addAction(m_freezeAllSelectedAction);
|
|
|
|
menu->addAction(m_thawAllSelectedAction);
|
2021-06-28 16:05:21 +00:00
|
|
|
menu->addAction(m_sweepOutputsAction);
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
|
|
|
else {
|
2021-03-14 21:12:02 +00:00
|
|
|
CoinsInfo* c = this->currentEntry();
|
|
|
|
if (!c) return;
|
2020-10-07 10:36:04 +00:00
|
|
|
|
2021-03-14 21:12:02 +00:00
|
|
|
bool isSpent = c->spent();
|
|
|
|
bool isFrozen = c->frozen();
|
|
|
|
bool isUnlocked = c->unlocked();
|
2020-10-07 10:36:04 +00:00
|
|
|
|
|
|
|
menu->addMenu(m_copyMenu);
|
|
|
|
|
|
|
|
if (!isSpent) {
|
|
|
|
isFrozen ? menu->addAction(m_thawOutputAction) : menu->addAction(m_freezeOutputAction);
|
2020-10-21 19:53:46 +00:00
|
|
|
|
2020-10-07 10:36:04 +00:00
|
|
|
menu->addAction(m_sweepOutputAction);
|
2020-10-21 19:53:46 +00:00
|
|
|
if (isFrozen || !isUnlocked) {
|
|
|
|
m_sweepOutputAction->setDisabled(true);
|
|
|
|
} else {
|
|
|
|
m_sweepOutputAction->setEnabled(true);
|
|
|
|
}
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
2020-10-21 19:53:46 +00:00
|
|
|
|
2020-10-07 10:36:04 +00:00
|
|
|
menu->addAction(m_viewOutputAction);
|
|
|
|
}
|
|
|
|
|
|
|
|
menu->popup(ui->coins->viewport()->mapToGlobal(point));
|
|
|
|
}
|
|
|
|
|
|
|
|
void CoinsWidget::showHeaderMenu(const QPoint& position)
|
|
|
|
{
|
|
|
|
m_headerMenu->popup(QCursor::pos());
|
|
|
|
}
|
|
|
|
|
|
|
|
void CoinsWidget::setShowSpent(bool show)
|
|
|
|
{
|
|
|
|
if(!m_proxyModel) return;
|
|
|
|
m_proxyModel->setShowSpent(show);
|
|
|
|
}
|
|
|
|
|
2021-06-28 17:20:16 +00:00
|
|
|
void CoinsWidget::setSearchFilter(const QString &filter) {
|
|
|
|
if (!m_proxyModel) return;
|
|
|
|
m_proxyModel->setSearchFilter(filter);
|
|
|
|
}
|
|
|
|
|
2020-10-07 10:36:04 +00:00
|
|
|
void CoinsWidget::freezeOutput() {
|
|
|
|
QModelIndex index = ui->coins->currentIndex();
|
2021-02-03 23:14:20 +00:00
|
|
|
QVector<int> indexes = {m_proxyModel->mapToSource(index).row()};
|
2021-05-02 18:22:38 +00:00
|
|
|
this->freezeCoins(indexes);
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CoinsWidget::freezeAllSelected() {
|
|
|
|
QModelIndexList list = ui->coins->selectionModel()->selectedRows();
|
|
|
|
|
|
|
|
QVector<int> indexes;
|
|
|
|
for (QModelIndex index: list) {
|
|
|
|
indexes.push_back(m_proxyModel->mapToSource(index).row()); // todo: will segfault if index get invalidated
|
|
|
|
}
|
2021-05-02 18:22:38 +00:00
|
|
|
this->freezeCoins(indexes);
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CoinsWidget::thawOutput() {
|
|
|
|
QModelIndex index = ui->coins->currentIndex();
|
2021-02-03 23:14:20 +00:00
|
|
|
QVector<int> indexes = {m_proxyModel->mapToSource(index).row()};
|
2021-05-02 18:22:38 +00:00
|
|
|
this->thawCoins(indexes);
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CoinsWidget::thawAllSelected() {
|
|
|
|
QModelIndexList list = ui->coins->selectionModel()->selectedRows();
|
|
|
|
|
|
|
|
QVector<int> indexes;
|
|
|
|
for (QModelIndex index: list) {
|
|
|
|
indexes.push_back(m_proxyModel->mapToSource(index).row());
|
|
|
|
}
|
2021-05-02 18:22:38 +00:00
|
|
|
this->thawCoins(indexes);
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CoinsWidget::viewOutput() {
|
2021-03-14 21:12:02 +00:00
|
|
|
CoinsInfo* c = this->currentEntry();
|
|
|
|
if (!c) return;
|
2020-10-07 10:36:04 +00:00
|
|
|
|
2021-03-14 21:12:02 +00:00
|
|
|
auto * dialog = new OutputInfoDialog(c, this);
|
|
|
|
dialog->show();
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void CoinsWidget::onSweepOutput() {
|
2021-03-14 21:12:02 +00:00
|
|
|
CoinsInfo* c = this->currentEntry();
|
|
|
|
if (!c) return;
|
2020-10-07 10:36:04 +00:00
|
|
|
|
2021-03-14 21:12:02 +00:00
|
|
|
QString keyImage = c->keyImage();
|
2020-10-07 10:36:04 +00:00
|
|
|
|
2021-03-14 21:12:02 +00:00
|
|
|
if (!c->keyImageKnown()) {
|
2020-12-14 02:20:05 +00:00
|
|
|
QMessageBox::warning(this, "Unable to sweep output", "Unable to sweep output: key image unknown");
|
2020-10-07 10:36:04 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-06-28 16:05:21 +00:00
|
|
|
auto *dialog = new OutputSweepDialog(this, c->amount());
|
2020-10-07 10:36:04 +00:00
|
|
|
int ret = dialog->exec();
|
|
|
|
if (!ret) return;
|
|
|
|
|
2021-05-02 18:22:38 +00:00
|
|
|
m_ctx->onSweepOutput(keyImage, dialog->address(), dialog->churn(), dialog->outputs());
|
2020-10-21 14:53:17 +00:00
|
|
|
dialog->deleteLater();
|
2020-10-07 10:36:04 +00:00
|
|
|
}
|
|
|
|
|
2021-06-28 16:05:21 +00:00
|
|
|
void CoinsWidget::onSweepMulti() {
|
|
|
|
QVector<CoinsInfo*> selectedCoins = this->currentEntries();
|
|
|
|
QVector<QString> keyImages;
|
|
|
|
|
|
|
|
quint64 totalAmount = 0;
|
|
|
|
for (const auto coin : selectedCoins) {
|
|
|
|
if (!coin) return;
|
|
|
|
|
|
|
|
QString keyImage = coin->keyImage();
|
|
|
|
if (!coin->keyImageKnown()) {
|
|
|
|
QMessageBox::warning(this, "Unable to sweep output", "Unable to sweep output: key image unknown");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
keyImages.push_back(keyImage);
|
|
|
|
totalAmount += coin->amount();
|
|
|
|
}
|
|
|
|
|
|
|
|
OutputSweepDialog dialog{this, totalAmount};
|
|
|
|
int ret = dialog.exec();
|
|
|
|
if (!ret) return;
|
|
|
|
|
|
|
|
m_ctx->onSweepOutputs(keyImages, dialog.address(), dialog.churn(), dialog.outputs());
|
|
|
|
}
|
|
|
|
|
2020-10-07 10:36:04 +00:00
|
|
|
void CoinsWidget::copy(copyField field) {
|
2021-03-14 21:12:02 +00:00
|
|
|
CoinsInfo* c = this->currentEntry();
|
|
|
|
if (!c) return;
|
2020-10-07 10:36:04 +00:00
|
|
|
|
|
|
|
QString data;
|
2021-03-14 21:12:02 +00:00
|
|
|
switch (field) {
|
|
|
|
case PubKey:
|
|
|
|
data = c->pubKey();
|
|
|
|
break;
|
|
|
|
case KeyImage:
|
|
|
|
data = c->keyImage();
|
|
|
|
break;
|
|
|
|
case TxID:
|
|
|
|
data = c->hash();
|
|
|
|
break;
|
|
|
|
case Address:
|
|
|
|
data = c->address();
|
|
|
|
break;
|
|
|
|
case Label:
|
|
|
|
data = c->addressLabel();
|
|
|
|
break;
|
|
|
|
case Height:
|
|
|
|
data = QString::number(c->blockHeight());
|
|
|
|
break;
|
|
|
|
case Amount:
|
|
|
|
data = c->displayAmount();
|
|
|
|
break;
|
|
|
|
}
|
2020-10-07 10:36:04 +00:00
|
|
|
|
|
|
|
Utils::copyToClipboard(data);
|
|
|
|
}
|
|
|
|
|
2021-03-14 21:12:02 +00:00
|
|
|
CoinsInfo* CoinsWidget::currentEntry() {
|
|
|
|
QModelIndexList list = ui->coins->selectionModel()->selectedRows();
|
|
|
|
if (list.size() == 1) {
|
|
|
|
return m_model->entryFromIndex(m_proxyModel->mapToSource(list.first()));
|
|
|
|
} else {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-28 16:05:21 +00:00
|
|
|
QVector<CoinsInfo*> CoinsWidget::currentEntries() {
|
|
|
|
QModelIndexList list = ui->coins->selectionModel()->selectedRows();
|
|
|
|
QVector<CoinsInfo*> selectedCoins;
|
|
|
|
for (const auto index : list) {
|
|
|
|
selectedCoins.push_back(m_model->entryFromIndex(m_proxyModel->mapToSource(index)));
|
|
|
|
}
|
|
|
|
return selectedCoins;
|
|
|
|
}
|
|
|
|
|
2021-05-02 18:22:38 +00:00
|
|
|
void CoinsWidget::freezeCoins(const QVector<int>& indexes) {
|
|
|
|
for (int i : indexes) {
|
2021-05-18 15:59:18 +00:00
|
|
|
m_ctx->wallet->coins()->freeze(i);
|
2021-05-02 18:22:38 +00:00
|
|
|
}
|
2021-05-18 15:59:18 +00:00
|
|
|
m_ctx->wallet->coins()->refresh(m_ctx->wallet->currentSubaddressAccount());
|
2021-05-02 18:22:38 +00:00
|
|
|
m_ctx->updateBalance();
|
|
|
|
}
|
|
|
|
|
|
|
|
void CoinsWidget::thawCoins(const QVector<int> &indexes) {
|
|
|
|
for (int i : indexes) {
|
2021-05-18 15:59:18 +00:00
|
|
|
m_ctx->wallet->coins()->thaw(i);
|
2021-05-02 18:22:38 +00:00
|
|
|
}
|
2021-05-18 15:59:18 +00:00
|
|
|
m_ctx->wallet->coins()->refresh(m_ctx->wallet->currentSubaddressAccount());
|
2021-05-02 18:22:38 +00:00
|
|
|
m_ctx->updateBalance();
|
|
|
|
}
|
|
|
|
|
2021-06-27 12:13:05 +00:00
|
|
|
CoinsWidget::~CoinsWidget() = default;
|