// SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020-2023 The Monero Project #include "CoinsWidget.h" #include "ui_CoinsWidget.h" #include #include "dialog/OutputInfoDialog.h" #include "dialog/OutputSweepDialog.h" #include "utils/Icons.h" #include "utils/Utils.h" CoinsWidget::CoinsWidget(Wallet *wallet, QWidget *parent) : QWidget(parent) , ui(new Ui::CoinsWidget) , m_wallet(wallet) , m_headerMenu(new QMenu(this)) , m_copyMenu(new QMenu("Copy",this)) { 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 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);}); m_copyMenu->addAction("Label", this, [this]{copy(copyField::Label);}); 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); m_editLabelAction = new QAction("Edit Label", this); connect(m_editLabelAction, &QAction::triggered, this, &CoinsWidget::editLabel); m_thawOutputAction = new QAction("Thaw output", this); m_freezeOutputAction = new QAction("Freeze output", this); m_freezeAllSelectedAction = new QAction("Freeze selected", this); m_thawAllSelectedAction = new QAction("Thaw selected", this); m_spendAction = new QAction("Spend", this); m_viewOutputAction = new QAction("Details", this); m_sweepOutputAction = new QAction("Sweep output", this); m_sweepOutputsAction = new QAction("Sweep selected outputs", this); connect(m_freezeOutputAction, &QAction::triggered, this, &CoinsWidget::freezeAllSelected); connect(m_thawOutputAction, &QAction::triggered, this, &CoinsWidget::thawAllSelected); connect(m_spendAction, &QAction::triggered, this, &CoinsWidget::spendSelected); connect(m_viewOutputAction, &QAction::triggered, this, &CoinsWidget::viewOutput); connect(m_sweepOutputAction, &QAction::triggered, this, &CoinsWidget::onSweepOutputs); connect(m_sweepOutputsAction, &QAction::triggered, this, &CoinsWidget::onSweepOutputs); connect(m_freezeAllSelectedAction, &QAction::triggered, this, &CoinsWidget::freezeAllSelected); connect(m_thawAllSelectedAction, &QAction::triggered, this, &CoinsWidget::thawAllSelected); connect(ui->coins, &QTreeView::customContextMenuRequested, this, &CoinsWidget::showContextMenu); connect(ui->coins, &QTreeView::doubleClicked, [this](QModelIndex index){ if (!m_model) return; if (!(m_model->flags(index) & Qt::ItemIsEditable)) { this->viewOutput(); } }); connect(ui->search, &QLineEdit::textChanged, this, &CoinsWidget::setSearchFilter); connect(m_wallet, &Wallet::selectedInputsChanged, this, &CoinsWidget::selectCoins); } void CoinsWidget::setModel(CoinsModel * model, Coins * coins) { m_coins = coins; m_model = model; m_proxyModel = new CoinsProxyModel(this, m_coins); 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); if (!m_wallet->viewOnly()) { ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, true); } else { ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, false); } ui->coins->header()->setSectionResizeMode(QHeaderView::ResizeToContents); ui->coins->header()->setSectionResizeMode(CoinsModel::Label, QHeaderView::Stretch); ui->coins->header()->setSortIndicator(CoinsModel::BlockHeight, Qt::DescendingOrder); ui->coins->setSortingEnabled(true); } void CoinsWidget::setSearchbarVisible(bool visible) { ui->search->setVisible(visible); } void CoinsWidget::focusSearchbar() { ui->search->setFocusPolicy(Qt::StrongFocus); ui->search->setFocus(); } 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_spendAction); menu->addAction(m_freezeAllSelectedAction); menu->addAction(m_thawAllSelectedAction); menu->addAction(m_sweepOutputsAction); } else { CoinsInfo* c = this->currentEntry(); if (!c) return; bool isSpent = c->spent(); bool isFrozen = c->frozen(); bool isUnlocked = c->unlocked(); menu->addAction(m_spendAction); menu->addMenu(m_copyMenu); menu->addAction(m_editLabelAction); if (!isSpent) { isFrozen ? menu->addAction(m_thawOutputAction) : menu->addAction(m_freezeOutputAction); menu->addAction(m_sweepOutputAction); if (isFrozen || !isUnlocked) { m_sweepOutputAction->setDisabled(true); } else { m_sweepOutputAction->setEnabled(true); } } 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); } void CoinsWidget::setSearchFilter(const QString &filter) { if (!m_proxyModel) return; m_proxyModel->setSearchFilter(filter); } QStringList CoinsWidget::selectedPubkeys() { QModelIndexList list = ui->coins->selectionModel()->selectedRows(); QStringList pubkeys; for (QModelIndex index: list) { pubkeys << m_model->entryFromIndex(m_proxyModel->mapToSource(index))->pubKey(); } return pubkeys; } void CoinsWidget::freezeAllSelected() { QStringList pubkeys = this->selectedPubkeys(); this->freezeCoins(pubkeys); } void CoinsWidget::thawAllSelected() { QStringList pubkeys = this->selectedPubkeys(); this->thawCoins(pubkeys); } void CoinsWidget::spendSelected() { QModelIndexList list = ui->coins->selectionModel()->selectedRows(); QStringList keyimages; for (QModelIndex index: list) { keyimages << m_model->entryFromIndex(m_proxyModel->mapToSource(index))->keyImage(); } m_wallet->setSelectedInputs(keyimages); this->selectCoins(keyimages); } void CoinsWidget::viewOutput() { CoinsInfo* c = this->currentEntry(); if (!c) return; auto * dialog = new OutputInfoDialog(c, this); dialog->show(); } void CoinsWidget::onSweepOutputs() { QVector selectedCoins = this->currentEntries(); QVector 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 outputs", "Unable to create transaction: selected output has unknown key image"); return; } if (coin->spent()) { QMessageBox::warning(this, "Unable to sweep outputs", "Unable to create transaction: selected output was already spent"); return; } if (coin->frozen()) { QMessageBox::warning(this, "Unable to sweep outputs", "Unable to create transaction: selected output is frozen.\n\n" "Thaw the selected output(s) before spending."); return; } if (!coin->unlocked()) { QMessageBox::warning(this, "Unable to sweep outputs", "Unable to create transaction: selected output is locked.\n\n" "Wait until the output has reached the required number of confirmation before spending."); return; } keyImages.push_back(keyImage); totalAmount += coin->amount(); } OutputSweepDialog dialog{this, totalAmount}; int ret = dialog.exec(); if (!ret) return; m_wallet->sweepOutputs(keyImages, dialog.address(), dialog.churn(), dialog.outputs()); } void CoinsWidget::copy(copyField field) { CoinsInfo* c = this->currentEntry(); if (!c) return; QString data; 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: { if (!c->description().isEmpty()) data = c->description(); else data = c->addressLabel(); break; } case Height: data = QString::number(c->blockHeight()); break; case Amount: data = c->displayAmount(); break; } Utils::copyToClipboard(data); } 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; } } QVector CoinsWidget::currentEntries() { QModelIndexList list = ui->coins->selectionModel()->selectedRows(); QVector selectedCoins; for (const auto index : list) { selectedCoins.push_back(m_model->entryFromIndex(m_proxyModel->mapToSource(index))); } return selectedCoins; } void CoinsWidget::freezeCoins(QStringList &pubkeys) { for (auto &pubkey : pubkeys) { m_wallet->coins()->freeze(pubkey); } m_wallet->coins()->refresh(m_wallet->currentSubaddressAccount()); m_wallet->updateBalance(); } void CoinsWidget::thawCoins(QStringList &pubkeys) { for (auto &pubkey : pubkeys) { m_wallet->coins()->thaw(pubkey); } m_wallet->coins()->refresh(m_wallet->currentSubaddressAccount()); m_wallet->updateBalance(); } void CoinsWidget::selectCoins(const QStringList &keyimages) { m_model->setSelected(keyimages); ui->coins->clearSelection(); } void CoinsWidget::editLabel() { QModelIndex index = ui->coins->currentIndex().siblingAtColumn(m_model->ModelColumn::Label); ui->coins->setCurrentIndex(index); ui->coins->edit(index); } CoinsWidget::~CoinsWidget() = default;