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(ui->search, &QLineEdit::textChanged, this, &CoinsWidget::setSearchFilter);
+
+ connect(m_ctx.get(), &AppContext::selectedInputsChanged, this, &CoinsWidget::selectCoins);
}
void CoinsWidget::setModel(CoinsModel * model, Coins * coins) {
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);
bool isFrozen = c->frozen();
bool isUnlocked = c->unlocked();
+ menu->addAction(m_spendAction);
menu->addMenu(m_copyMenu);
menu->addAction(m_editLabelAction);
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;
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);
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();
void setShowSpent(bool show);
void freezeAllSelected();
void thawAllSelected();
+ void spendSelected();
void viewOutput();
void onSweepOutputs();
void setSearchFilter(const QString &filter);
private:
void freezeCoins(QStringList &pubkeys);
void thawCoins(QStringList &pubkeys);
+ void selectCoins(const QStringList &pubkeys);
enum copyField {
PubKey = 0,
QMenu *m_contextMenu;
QMenu *m_headerMenu;
QMenu *m_copyMenu;
+ QAction *m_spendAction;
QAction *m_showSpentAction;
QAction *m_freezeOutputAction;
QAction *m_freezeAllSelectedAction;
#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"
#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() {
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);
}
}
+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;
void onEndTransaction();
void onCustomRestoreHeightSet(int height);
void onKeysCorrupted();
+ void onSelectedInputsChanged(const QStringList &selectedInputs);
// libwalletqt
void onBalanceUpdated(quint64 balance, quint64 spendable);
</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"/>
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();
}
}
qInfo() << "Creating transaction";
- this->wallet->createTransactionMultiDestAsync(addresses, amounts, this->tx_priority);
+ this->wallet->createTransactionMultiDestAsync(addresses, amounts, this->tx_priority, m_selectedInputs);
emit initiateTransaction();
}
}
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()) {
// ################## Misc ##################
+void AppContext::setSelectedInputs(const QStringList &selectedInputs) {
+ m_selectedInputs = selectedInputs;
+ emit selectedInputsChanged(selectedInputs);
+}
+
void AppContext::onTorSettingsChanged() {
if (Utils::isTorsocks()) {
return;
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);
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
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());
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;
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();
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();
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();
}
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);
}
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();
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);
});
//! 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,
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);
}
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()) {
else if (cInfo.spent()) {
result = "Output is spent";
}
+ else if (selected) {
+ result = "Coin selected to be spent";
+ }
}
});
if (!found) {
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());
CoinsInfo* entryFromIndex(const QModelIndex &index) const;
void setCurrentSubaddressAccount(quint32 accountIndex);
+ void setSelected(const QStringList &selected);
signals:
void descriptionChanged();
Coins *m_coins;
quint32 m_currentSubaddressAccount;
+ QSet<QString> m_selected;
};
#endif //FEATHER_COINSMODEL_H