From 3eb6d282219e745f615c3b5836f724a9789372b8 Mon Sep 17 00:00:00 2001 From: tobtoht Date: Sun, 20 Mar 2022 23:30:48 +0100 Subject: [PATCH] Coins: manual input selection --- src/CoinsWidget.cpp | 23 +++++++++++++++++++ src/CoinsWidget.h | 8 +++++++ src/MainWindow.cpp | 25 +++++++++++++++++++++ src/MainWindow.h | 1 + src/MainWindow.ui | 41 +++++++++++++++++++++++++++++++++ src/appcontext.cpp | 14 +++++++++--- src/appcontext.h | 4 ++++ src/libwalletqt/Coins.cpp | 13 +++++++++++ src/libwalletqt/Coins.h | 18 +++++++-------- src/libwalletqt/Wallet.cpp | 46 +++++++++++++++++++++++++------------- src/libwalletqt/Wallet.h | 19 +++++++++------- src/model/CoinsModel.cpp | 16 +++++++++++++ src/model/CoinsModel.h | 2 ++ 13 files changed, 195 insertions(+), 35 deletions(-) diff --git a/src/CoinsWidget.cpp b/src/CoinsWidget.cpp index 4afc22d..6b623a4 100644 --- a/src/CoinsWidget.cpp +++ b/src/CoinsWidget.cpp @@ -46,12 +46,14 @@ CoinsWidget::CoinsWidget(QSharedPointer ctx, QWidget *parent) 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); @@ -68,6 +70,8 @@ CoinsWidget::CoinsWidget(QSharedPointer ctx, QWidget *parent) }); connect(ui->search, &QLineEdit::textChanged, this, &CoinsWidget::setSearchFilter); + + connect(m_ctx.get(), &AppContext::selectedInputsChanged, this, &CoinsWidget::selectCoins); } void CoinsWidget::setModel(CoinsModel * model, Coins * coins) { @@ -106,6 +110,7 @@ void CoinsWidget::showContextMenu(const QPoint &point) { 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); @@ -118,6 +123,7 @@ void CoinsWidget::showContextMenu(const QPoint &point) { bool isFrozen = c->frozen(); bool isUnlocked = c->unlocked(); + menu->addAction(m_spendAction); menu->addMenu(m_copyMenu); menu->addAction(m_editLabelAction); @@ -174,6 +180,18 @@ void CoinsWidget::thawAllSelected() { 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_ctx->setSelectedInputs(keyimages); + this->selectCoins(keyimages); +} + void CoinsWidget::viewOutput() { CoinsInfo* c = this->currentEntry(); if (!c) return; @@ -294,6 +312,11 @@ void CoinsWidget::thawCoins(QStringList &pubkeys) { m_ctx->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); diff --git a/src/CoinsWidget.h b/src/CoinsWidget.h index 0c4be12..f0978d7 100644 --- a/src/CoinsWidget.h +++ b/src/CoinsWidget.h @@ -26,6 +26,11 @@ public: void setModel(CoinsModel * model, Coins * coins); ~CoinsWidget() override; + void setSpendSelected(const QStringList &pubkeys); + +signals: + void spendSelectedChanged(const QStringList &pubkeys); + public slots: void setSearchbarVisible(bool visible); void focusSearchbar(); @@ -35,6 +40,7 @@ private slots: void setShowSpent(bool show); void freezeAllSelected(); void thawAllSelected(); + void spendSelected(); void viewOutput(); void onSweepOutputs(); void setSearchFilter(const QString &filter); @@ -43,6 +49,7 @@ private slots: private: void freezeCoins(QStringList &pubkeys); void thawCoins(QStringList &pubkeys); + void selectCoins(const QStringList &pubkeys); enum copyField { PubKey = 0, @@ -60,6 +67,7 @@ private: QMenu *m_contextMenu; QMenu *m_headerMenu; QMenu *m_copyMenu; + QAction *m_spendAction; QAction *m_showSpentAction; QAction *m_freezeOutputAction; QAction *m_freezeAllSelectedAction; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index f509cc7..c0a010b 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -25,6 +25,7 @@ #include "dialog/WalletCacheDebugDialog.h" #include "dialog/UpdateDialog.h" #include "libwalletqt/AddressBook.h" +#include "libwalletqt/CoinsInfo.h" #include "libwalletqt/Transfer.h" #include "utils/AppData.h" #include "utils/AsyncTask.h" @@ -217,6 +218,11 @@ void MainWindow::initWidgets() { #if defined(Q_OS_MACOS) ui->line->hide(); #endif + + ui->frame_coinControl->setVisible(false); + connect(ui->btn_resetCoinControl, &QPushButton::clicked, [this]{ + m_ctx->setSelectedInputs({}); + }); } void MainWindow::initMenu() { @@ -384,6 +390,7 @@ void MainWindow::initWalletContext() { connect(m_ctx.get(), &AppContext::endTransaction, this, &MainWindow::onEndTransaction); connect(m_ctx.get(), &AppContext::customRestoreHeightSet, this, &MainWindow::onCustomRestoreHeightSet); connect(m_ctx.get(), &AppContext::keysCorrupted, this, &MainWindow::onKeysCorrupted); + connect(m_ctx.get(), &AppContext::selectedInputsChanged, this, &MainWindow::onSelectedInputsChanged); // Nodes connect(m_ctx->nodes, &Nodes::nodeExhausted, this, &MainWindow::showNodeExhaustedMessage); @@ -1446,6 +1453,24 @@ void MainWindow::onKeysCorrupted() { } } +void MainWindow::onSelectedInputsChanged(const QStringList &selectedInputs) { + int numInputs = selectedInputs.size(); + + ui->frame_coinControl->setStyleSheet(ColorScheme::GREEN.asStylesheet(true)); + ui->frame_coinControl->setVisible(numInputs > 0); + + if (numInputs > 0) { + quint64 totalAmount = 0; + auto coins = m_ctx->wallet->coins()->coinsFromKeyImage(selectedInputs); + for (const auto coin : coins) { + totalAmount += coin->amount(); + } + + QString text = QString("Coin control active: %1 selected outputs, %2 XMR").arg(QString::number(numInputs), WalletManager::displayAmount(totalAmount)); + ui->label_coinControl->setText(text); + } +} + void MainWindow::onExportHistoryCSV(bool checked) { if (m_ctx->wallet == nullptr) return; diff --git a/src/MainWindow.h b/src/MainWindow.h index a8b3464..0b24c2c 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -140,6 +140,7 @@ private slots: void onEndTransaction(); void onCustomRestoreHeightSet(int height); void onKeysCorrupted(); + void onSelectedInputsChanged(const QStringList &selectedInputs); // libwalletqt void onBalanceUpdated(quint64 balance, quint64 spendable); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 3d82bf1..9d1602e 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -343,6 +343,47 @@ + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + + + Coin control active: + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + Reset + + + + + + diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 15420b0..5230934 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -82,9 +82,9 @@ void AppContext::onCreateTransaction(const QString &address, quint64 amount, con qInfo() << "Creating transaction"; if (all) - this->wallet->createTransactionAllAsync(address, "", constants::mixin, this->tx_priority); + this->wallet->createTransactionAllAsync(address, "", constants::mixin, this->tx_priority, m_selectedInputs); else - this->wallet->createTransactionAsync(address, "", amount, constants::mixin, this->tx_priority); + this->wallet->createTransactionAsync(address, "", amount, constants::mixin, this->tx_priority, m_selectedInputs); emit initiateTransaction(); } @@ -103,7 +103,7 @@ void AppContext::onCreateTransactionMultiDest(const QVector &addresses, } qInfo() << "Creating transaction"; - this->wallet->createTransactionMultiDestAsync(addresses, amounts, this->tx_priority); + this->wallet->createTransactionMultiDestAsync(addresses, amounts, this->tx_priority, m_selectedInputs); emit initiateTransaction(); } @@ -131,6 +131,9 @@ void AppContext::onCancelTransaction(PendingTransaction *tx, const QVectorsetSelectedInputs({}); + // Nodes - even well-connected, properly configured ones - consistently fail to relay transactions // To mitigate transactions failing we just send the transaction to every node we know about over Tor if (config()->get(Config::multiBroadcast).toBool()) { @@ -180,6 +183,11 @@ void AppContext::onDeviceError(const QString &message) { // ################## Misc ################## +void AppContext::setSelectedInputs(const QStringList &selectedInputs) { + m_selectedInputs = selectedInputs; + emit selectedInputsChanged(selectedInputs); +} + void AppContext::onTorSettingsChanged() { if (Utils::isTorsocks()) { return; diff --git a/src/appcontext.h b/src/appcontext.h index dd6fcde..2035a84 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -49,6 +49,8 @@ public: void addCacheTransaction(const QString &txid, const QString &txHex) const; QString getCacheTransaction(const QString &txid) const; + void setSelectedInputs(const QStringList &selectedInputs); + public slots: void onCreateTransaction(const QString &address, quint64 amount, const QString &description, bool all); void onCreateTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description); @@ -93,10 +95,12 @@ signals: void deviceButtonPressed(); void deviceError(const QString &message); void keysCorrupted(); + void selectedInputsChanged(const QStringList &selectedInputs); private: DaemonRpc *m_rpc; QTimer m_storeTimer; + QStringList m_selectedInputs; }; #endif //FEATHER_APPCONTEXT_H diff --git a/src/libwalletqt/Coins.cpp b/src/libwalletqt/Coins.cpp index f000bb0..66f5206 100644 --- a/src/libwalletqt/Coins.cpp +++ b/src/libwalletqt/Coins.cpp @@ -96,6 +96,19 @@ QVector Coins::coins_from_txid(const QString &txid) return coins; } +QVector Coins::coinsFromKeyImage(const QStringList &keyimages) { + QVector coins; + + for (int i = 0; i < this->count(); i++) { + CoinsInfo* coin = this->coin(i); + if (coin->keyImageKnown() && keyimages.contains(coin->keyImage())) { + coins.append(coin); + } + } + + return coins; +} + void Coins::setDescription(const QString &publicKey, quint32 accountIndex, const QString &description) { m_pimpl->setDescription(publicKey.toStdString(), description.toStdString()); diff --git a/src/libwalletqt/Coins.h b/src/libwalletqt/Coins.h index efde480..3af24cf 100644 --- a/src/libwalletqt/Coins.h +++ b/src/libwalletqt/Coins.h @@ -21,17 +21,17 @@ class CoinsInfo; class Coins : public QObject { Q_OBJECT - Q_PROPERTY(int count READ count) public: - Q_INVOKABLE bool coin(int index, std::function callback); - Q_INVOKABLE CoinsInfo * coin(int index); - Q_INVOKABLE void refresh(quint32 accountIndex); - Q_INVOKABLE void refreshUnlocked(); - Q_INVOKABLE void freeze(QString &publicKey) const; - Q_INVOKABLE void thaw(QString &publicKey) const; - Q_INVOKABLE QVector coins_from_txid(const QString &txid); - Q_INVOKABLE void setDescription(const QString &publicKey, quint32 accountIndex, const QString &description); + bool coin(int index, std::function callback); + CoinsInfo * coin(int index); + void refresh(quint32 accountIndex); + void refreshUnlocked(); + void freeze(QString &publicKey) const; + void thaw(QString &publicKey) const; + QVector coins_from_txid(const QString &txid); + QVector coinsFromKeyImage(const QStringList &keyimages); + void setDescription(const QString &publicKey, quint32 accountIndex, const QString &description); quint64 count() const; diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 6a60cd1..e996fca 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -609,14 +609,18 @@ void Wallet::pauseRefresh() PendingTransaction *Wallet::createTransaction(const QString &dst_addr, const QString &payment_id, quint64 amount, quint32 mixin_count, - PendingTransaction::Priority priority) + PendingTransaction::Priority priority, const QStringList &preferredInputs) { // pauseRefresh(); + std::set preferred_inputs; + for (const auto &input : preferredInputs) { + preferred_inputs.insert(input.toStdString()); + } std::set subaddr_indices; Monero::PendingTransaction * ptImpl = m_walletImpl->createTransaction( dst_addr.toStdString(), payment_id.toStdString(), amount, mixin_count, - static_cast(priority), currentSubaddressAccount(), subaddr_indices); + static_cast(priority), currentSubaddressAccount(), subaddr_indices, preferred_inputs); PendingTransaction * result = new PendingTransaction(ptImpl, nullptr); // startRefresh(); @@ -625,17 +629,17 @@ PendingTransaction *Wallet::createTransaction(const QString &dst_addr, const QSt void Wallet::createTransactionAsync(const QString &dst_addr, const QString &payment_id, quint64 amount, quint32 mixin_count, - PendingTransaction::Priority priority) + PendingTransaction::Priority priority, const QStringList &preferredInputs) { - m_scheduler.run([this, dst_addr, payment_id, amount, mixin_count, priority] { - PendingTransaction *tx = createTransaction(dst_addr, payment_id, amount, mixin_count, priority); + m_scheduler.run([this, dst_addr, payment_id, amount, mixin_count, priority, preferredInputs] { + PendingTransaction *tx = createTransaction(dst_addr, payment_id, amount, mixin_count, priority, preferredInputs); QVector address {dst_addr}; emit transactionCreated(tx, address); }); } PendingTransaction* Wallet::createTransactionMultiDest(const QVector &dst_addr, const QVector &amount, - PendingTransaction::Priority priority) + PendingTransaction::Priority priority, const QStringList &preferredInputs) { // pauseRefresh(); @@ -649,8 +653,14 @@ PendingTransaction* Wallet::createTransactionMultiDest(const QVector &d amounts.push_back(a); } + std::set preferred_inputs; + for (const auto &input : preferredInputs) { + preferred_inputs.insert(input.toStdString()); + } + // TODO: remove mixin count - Monero::PendingTransaction * ptImpl = m_walletImpl->createTransactionMultDest(dests, "", amounts, 11, static_cast(priority)); + std::set subaddr_indices; + Monero::PendingTransaction * ptImpl = m_walletImpl->createTransactionMultDest(dests, "", amounts, 11, static_cast(priority), currentSubaddressAccount(), subaddr_indices, preferred_inputs); PendingTransaction * result = new PendingTransaction(ptImpl); // startRefresh(); @@ -658,10 +668,10 @@ PendingTransaction* Wallet::createTransactionMultiDest(const QVector &d } void Wallet::createTransactionMultiDestAsync(const QVector &dst_addr, const QVector &amount, - PendingTransaction::Priority priority) + PendingTransaction::Priority priority, const QStringList &preferredInputs) { - m_scheduler.run([this, dst_addr, amount, priority] { - PendingTransaction *tx = createTransactionMultiDest(dst_addr, amount, priority); + m_scheduler.run([this, dst_addr, amount, priority, preferredInputs] { + PendingTransaction *tx = createTransactionMultiDest(dst_addr, amount, priority, preferredInputs); QVector addresses; for (auto &addr : dst_addr) { addresses.push_back(addr); @@ -671,14 +681,20 @@ void Wallet::createTransactionMultiDestAsync(const QVector &dst_addr, c } PendingTransaction *Wallet::createTransactionAll(const QString &dst_addr, const QString &payment_id, - quint32 mixin_count, PendingTransaction::Priority priority) + quint32 mixin_count, PendingTransaction::Priority priority, + const QStringList &preferredInputs) { // pauseRefresh(); + std::set preferred_inputs; + for (const auto &input : preferredInputs) { + preferred_inputs.insert(input.toStdString()); + } + std::set subaddr_indices; Monero::PendingTransaction * ptImpl = m_walletImpl->createTransaction( dst_addr.toStdString(), payment_id.toStdString(), Monero::optional(), mixin_count, - static_cast(priority), currentSubaddressAccount(), subaddr_indices); + static_cast(priority), currentSubaddressAccount(), subaddr_indices, preferred_inputs); PendingTransaction * result = new PendingTransaction(ptImpl, this); // startRefresh(); @@ -687,10 +703,10 @@ PendingTransaction *Wallet::createTransactionAll(const QString &dst_addr, const void Wallet::createTransactionAllAsync(const QString &dst_addr, const QString &payment_id, quint32 mixin_count, - PendingTransaction::Priority priority) + PendingTransaction::Priority priority, const QStringList &preferredInputs) { - m_scheduler.run([this, dst_addr, payment_id, mixin_count, priority] { - PendingTransaction *tx = createTransactionAll(dst_addr, payment_id, mixin_count, priority); + m_scheduler.run([this, dst_addr, payment_id, mixin_count, priority, preferredInputs] { + PendingTransaction *tx = createTransactionAll(dst_addr, payment_id, mixin_count, priority, preferredInputs); QVector address {dst_addr}; emit transactionCreated(tx, address); }); diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 9b08f5e..ed30083 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -271,30 +271,33 @@ public: //! creates transaction PendingTransaction * createTransaction(const QString &dst_addr, const QString &payment_id, - quint64 amount, quint32 mixin_count, - PendingTransaction::Priority priority); + quint64 amount, quint32 mixin_count, + PendingTransaction::Priority priority, + const QStringList &preferredInputs); //! creates async transaction void createTransactionAsync(const QString &dst_addr, const QString &payment_id, - quint64 amount, quint32 mixin_count, - PendingTransaction::Priority priority); + quint64 amount, quint32 mixin_count, + PendingTransaction::Priority priority, const QStringList &preferredInputs); //! creates multi-destination transaction PendingTransaction * createTransactionMultiDest(const QVector &dst_addr, const QVector &amount, - PendingTransaction::Priority priority); + PendingTransaction::Priority priority, const QStringList &preferredInputs); //! creates async multi-destination transaction void createTransactionMultiDestAsync(const QVector &dst_addr, const QVector &amount, - PendingTransaction::Priority priority); + PendingTransaction::Priority priority, const QStringList &preferredInputs); //! creates transaction with all outputs PendingTransaction * createTransactionAll(const QString &dst_addr, const QString &payment_id, - quint32 mixin_count, PendingTransaction::Priority priority); + quint32 mixin_count, PendingTransaction::Priority priority, + const QStringList &preferredInputs); //! creates async transaction with all outputs void createTransactionAllAsync(const QString &dst_addr, const QString &payment_id, - quint32 mixin_count, PendingTransaction::Priority priority); + quint32 mixin_count, PendingTransaction::Priority priority, + const QStringList &preferredInputs); //! creates transaction with single input PendingTransaction * createTransactionSingle(const QString &key_image, const QString &dst_addr, diff --git a/src/model/CoinsModel.cpp b/src/model/CoinsModel.cpp index b099676..45c2949 100644 --- a/src/model/CoinsModel.cpp +++ b/src/model/CoinsModel.cpp @@ -61,6 +61,8 @@ QVariant CoinsModel::data(const QModelIndex &index, int role) const QVariant result; bool found = m_coins->coin(index.row(), [this, &index, &result, &role](const CoinsInfo &cInfo) { + bool selected = cInfo.keyImageKnown() && m_selected.contains(cInfo.keyImage()); + if(role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole) { result = parseTransactionInfo(cInfo, index.column(), role); } @@ -74,6 +76,9 @@ QVariant CoinsModel::data(const QModelIndex &index, int role) const else if (!cInfo.unlocked()) { result = QBrush(ColorScheme::YELLOW.asColor(true)); } + else if (selected) { + result = QBrush(ColorScheme::GREEN.asColor(true)); + } } else if (role == Qt::TextAlignmentRole) { switch (index.column()) { @@ -122,6 +127,9 @@ QVariant CoinsModel::data(const QModelIndex &index, int role) const else if (cInfo.spent()) { result = "Output is spent"; } + else if (selected) { + result = "Coin selected to be spent"; + } } }); if (!found) { @@ -247,6 +255,14 @@ void CoinsModel::setCurrentSubaddressAccount(quint32 accountIndex) { m_currentSubaddressAccount = accountIndex; } +void CoinsModel::setSelected(const QStringList &keyimages) { + m_selected.clear(); + for (const auto &ki : keyimages) { + m_selected.insert(ki); + } + emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); +} + CoinsInfo* CoinsModel::entryFromIndex(const QModelIndex &index) const { Q_ASSERT(index.isValid() && index.row() < m_coins->count()); return m_coins->coin(index.row()); diff --git a/src/model/CoinsModel.h b/src/model/CoinsModel.h index ce9a62b..2e17e8a 100644 --- a/src/model/CoinsModel.h +++ b/src/model/CoinsModel.h @@ -48,6 +48,7 @@ public: CoinsInfo* entryFromIndex(const QModelIndex &index) const; void setCurrentSubaddressAccount(quint32 accountIndex); + void setSelected(const QStringList &selected); signals: void descriptionChanged(); @@ -61,6 +62,7 @@ private: Coins *m_coins; quint32 m_currentSubaddressAccount; + QSet m_selected; }; #endif //FEATHER_COINSMODEL_H