Coins: manual input selection

This commit is contained in:
tobtoht 2022-03-20 23:30:48 +01:00
parent b8db796479
commit 3eb6d28221
No known key found for this signature in database
GPG key ID: 1CADD27F41F45C3C
13 changed files with 195 additions and 35 deletions

View file

@ -46,12 +46,14 @@ CoinsWidget::CoinsWidget(QSharedPointer<AppContext> 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<AppContext> 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);

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -343,6 +343,47 @@
</widget>
</widget>
</item>
<item row="2" column="0">
<widget class="QFrame" name="frame_coinControl">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_coinControl">
<property name="text">
<string>Coin control active: </string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_resetCoinControl">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusBar"/>

View file

@ -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<QString> &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 QVector<QStri
}
void AppContext::commitTransaction(PendingTransaction *tx, const QString &description) {
// Clear list of selected transfers
this->setSelectedInputs({});
// 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;

View file

@ -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<QString> &addresses, const QVector<quint64> &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

View file

@ -96,6 +96,19 @@ QVector<CoinsInfo*> Coins::coins_from_txid(const QString &txid)
return coins;
}
QVector<CoinsInfo*> Coins::coinsFromKeyImage(const QStringList &keyimages) {
QVector<CoinsInfo*> 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());

View file

@ -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<void (CoinsInfo &)> 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<CoinsInfo*> coins_from_txid(const QString &txid);
Q_INVOKABLE void setDescription(const QString &publicKey, quint32 accountIndex, const QString &description);
bool coin(int index, std::function<void (CoinsInfo &)> callback);
CoinsInfo * coin(int index);
void refresh(quint32 accountIndex);
void refreshUnlocked();
void freeze(QString &publicKey) const;
void thaw(QString &publicKey) const;
QVector<CoinsInfo*> coins_from_txid(const QString &txid);
QVector<CoinsInfo*> coinsFromKeyImage(const QStringList &keyimages);
void setDescription(const QString &publicKey, quint32 accountIndex, const QString &description);
quint64 count() const;

View file

@ -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<std::string> preferred_inputs;
for (const auto &input : preferredInputs) {
preferred_inputs.insert(input.toStdString());
}
std::set<uint32_t> subaddr_indices;
Monero::PendingTransaction * ptImpl = m_walletImpl->createTransaction(
dst_addr.toStdString(), payment_id.toStdString(), amount, mixin_count,
static_cast<Monero::PendingTransaction::Priority>(priority), currentSubaddressAccount(), subaddr_indices);
static_cast<Monero::PendingTransaction::Priority>(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<QString> address {dst_addr};
emit transactionCreated(tx, address);
});
}
PendingTransaction* Wallet::createTransactionMultiDest(const QVector<QString> &dst_addr, const QVector<quint64> &amount,
PendingTransaction::Priority priority)
PendingTransaction::Priority priority, const QStringList &preferredInputs)
{
// pauseRefresh();
@ -649,8 +653,14 @@ PendingTransaction* Wallet::createTransactionMultiDest(const QVector<QString> &d
amounts.push_back(a);
}
std::set<std::string> 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<Monero::PendingTransaction::Priority>(priority));
std::set<uint32_t> subaddr_indices;
Monero::PendingTransaction * ptImpl = m_walletImpl->createTransactionMultDest(dests, "", amounts, 11, static_cast<Monero::PendingTransaction::Priority>(priority), currentSubaddressAccount(), subaddr_indices, preferred_inputs);
PendingTransaction * result = new PendingTransaction(ptImpl);
// startRefresh();
@ -658,10 +668,10 @@ PendingTransaction* Wallet::createTransactionMultiDest(const QVector<QString> &d
}
void Wallet::createTransactionMultiDestAsync(const QVector<QString> &dst_addr, const QVector<quint64> &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<QString> addresses;
for (auto &addr : dst_addr) {
addresses.push_back(addr);
@ -671,14 +681,20 @@ void Wallet::createTransactionMultiDestAsync(const QVector<QString> &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<std::string> preferred_inputs;
for (const auto &input : preferredInputs) {
preferred_inputs.insert(input.toStdString());
}
std::set<uint32_t> subaddr_indices;
Monero::PendingTransaction * ptImpl = m_walletImpl->createTransaction(
dst_addr.toStdString(), payment_id.toStdString(), Monero::optional<uint64_t>(), mixin_count,
static_cast<Monero::PendingTransaction::Priority>(priority), currentSubaddressAccount(), subaddr_indices);
static_cast<Monero::PendingTransaction::Priority>(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<QString> address {dst_addr};
emit transactionCreated(tx, address);
});

View file

@ -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<QString> &dst_addr, const QVector<quint64> &amount,
PendingTransaction::Priority priority);
PendingTransaction::Priority priority, const QStringList &preferredInputs);
//! creates async multi-destination transaction
void createTransactionMultiDestAsync(const QVector<QString> &dst_addr, const QVector<quint64> &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,

View file

@ -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());

View file

@ -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<QString> m_selected;
};
#endif //FEATHER_COINSMODEL_H