From 8efed3bc93a15fc715fd4f3cca50f44685872b5e Mon Sep 17 00:00:00 2001 From: Jonas Schnelli Date: Thu, 17 Mar 2016 17:54:54 +0100 Subject: [PATCH] [Qt] Support for abandoned/abandoning transactions --- src/Makefile.qt.include | 3 +- src/qt/bitcoin.qrc | 1 + src/qt/guiconstants.h | 2 ++ src/qt/res/icons/transaction_abandoned.png | Bin 0 -> 1473 bytes src/qt/transactiondesc.cpp | 2 +- src/qt/transactionrecord.cpp | 2 ++ src/qt/transactionrecord.h | 1 + src/qt/transactiontablemodel.cpp | 10 +++++++ src/qt/transactionview.cpp | 33 +++++++++++++++++++-- src/qt/transactionview.h | 2 ++ src/qt/walletmodel.cpp | 15 ++++++++++ src/qt/walletmodel.h | 3 ++ 12 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/qt/res/icons/transaction_abandoned.png diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index ca4e1e70d..247ca3f1d 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -268,7 +268,8 @@ RES_ICONS = \ qt/res/icons/tx_output.png \ qt/res/icons/tx_mined.png \ qt/res/icons/warning.png \ - qt/res/icons/verify.png + qt/res/icons/verify.png \ + qt/res/icons/transaction_abandoned.png BITCOIN_QT_CPP = \ qt/bantablemodel.cpp \ diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index dcd3b4ae2..24b0bae3e 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -49,6 +49,7 @@ res/icons/fontbigger.png res/icons/fontsmaller.png res/icons/chevron.png + res/icons/transaction_abandoned.png res/movies/spinner-000.png diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h index 5ceffcd70..4b2c10dd4 100644 --- a/src/qt/guiconstants.h +++ b/src/qt/guiconstants.h @@ -29,6 +29,8 @@ static const bool DEFAULT_SPLASHSCREEN = true; #define COLOR_TX_STATUS_OPENUNTILDATE QColor(64, 64, 255) /* Transaction list -- TX status decoration - offline */ #define COLOR_TX_STATUS_OFFLINE QColor(192, 192, 192) +/* Transaction list -- TX status decoration - danger, tx needs attention */ +#define COLOR_TX_STATUS_DANGER QColor(200, 100, 100) /* Transaction list -- TX status decoration - default color */ #define COLOR_BLACK QColor(0, 0, 0) diff --git a/src/qt/res/icons/transaction_abandoned.png b/src/qt/res/icons/transaction_abandoned.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca6445c20f263b3dd479806a524e3f09b8f9e63 GIT binary patch literal 1473 zcmchX`8(8k0LMSynaoU+ab$|ek)tO~D$$@ah8aiFk|SEjIxB=3gH0I63^{UbMX?sC zZJ%}I2uX;qb*;sq7)x%$jF53HQLLVRY5#$Jp7;Cx`uY9K>yFhWQyf+n3jn~GT`;!U zCF@HusNHSSckci|B*Dy>Y8x`NFhcdl-@>;o`R}hiX`$rdLBrX{x>Tyos9;o{;Ex$5 zD5rafqo_i{1@%d)M6|T>19{`)%?D%O3||+g5?1dU#vIaXDWuk*Djz(tRjwhg;w&EK z?9jI>JZB76wwg%i{7)~ww&adPsY(_0t0hjxWhnulXh7IQQ3hDZ3d1r4S(oUaebDtl zl{Lnarf83cxZgClcU~x_+q-~DXbc3g#p|p)9dc+0;m#(8zz-L`u&%1vCq7ObcW;bS zY%wZoC$*-hP9BNLq(BVb&Fn*9R>SdP1BzOBVj z5FUwaSYS%Y9HR`c3M>XUYt+*f(OdHpxld%~(z{b#z1NZdIrPoB4Fmy_W*M*y*ov%C zRLZtQ^_~;Jl3hf(6_ZUTeJGfAE?=p>QtI688XO^=|LUd_xR0!MK{_wWDxUWc_HV8f& zm|D!xuya#V^@8t7p%*d_d&PrF6s&H_xnRzTyt1uTLN(=VXJ$*9a?~LS@IQuS4pj@R zd9#^8v;dxH8*h~qDAvrAXYf|rc%r1hs%BpHKZ3579+1642FG|-XSMq zPXpTs7brFu@Ll6P?Jm)8Kx*1nzEs9O=OiS5vXr7v-sb`s4S-uSW?c$ zGVL5W_>-ip!4JBQa`QDP`BIJE!jM%@f);p3PJD)7{<%IhHJGvR7$QKWdf3;wL12I! zwv26?;#Xk7c$Mk%B)BSDEJ`ji%5lAF?x_Qc!CjgDAl+8`!D>LZsN;7Q6XRdbd$J9m1Zbk@h) z7WwvvFS14JuMIev*{_EawY?Mo+w0L(PE8iAYbx=AOZ*?3tmXR6B*lX~ zWG2n}a^n7qE`&G)V0w2qY?5p;Cmo6ajPp%2HO{lA1 2 * 60 && wtx.GetRequestCount() == 0) return tr("%1/offline").arg(nDepth); else if (nDepth == 0) - return tr("0/unconfirmed, %1").arg((wtx.InMempool() ? tr("in memory pool") : tr("not in memory pool"))); + return tr("0/unconfirmed, %1").arg((wtx.InMempool() ? tr("in memory pool") : tr("not in memory pool"))) + (wtx.isAbandoned() ? ", "+tr("abandoned") : ""); else if (nDepth < 6) return tr("%1/unconfirmed").arg(nDepth); else diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp index 5b16b108e..1efeda93e 100644 --- a/src/qt/transactionrecord.cpp +++ b/src/qt/transactionrecord.cpp @@ -239,6 +239,8 @@ void TransactionRecord::updateStatus(const CWalletTx &wtx) else if (status.depth == 0) { status.status = TransactionStatus::Unconfirmed; + if (wtx.isAbandoned()) + status.status = TransactionStatus::Abandoned; } else if (status.depth < RecommendedNumConfirmations) { diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h index 49753ee31..330cd48cf 100644 --- a/src/qt/transactionrecord.h +++ b/src/qt/transactionrecord.h @@ -33,6 +33,7 @@ public: Unconfirmed, /**< Not yet mined into a block **/ Confirming, /**< Confirmed, but waiting for the recommended number of confirmations **/ Conflicted, /**< Conflicts with other transaction or mempool **/ + Abandoned, /**< Abandoned from the wallet **/ /// Generated (mined) transactions Immature, /**< Mined but waiting for maturity */ MaturesWarning, /**< Transaction will likely not mature because no nodes have confirmed */ diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index d2a52b302..b29ecf834 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -312,6 +312,9 @@ QString TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) cons case TransactionStatus::Unconfirmed: status = tr("Unconfirmed"); break; + case TransactionStatus::Abandoned: + status = tr("Abandoned"); + break; case TransactionStatus::Confirming: status = tr("Confirming (%1 of %2 recommended confirmations)").arg(wtx->status.depth).arg(TransactionRecord::RecommendedNumConfirmations); break; @@ -468,6 +471,8 @@ QVariant TransactionTableModel::txStatusDecoration(const TransactionRecord *wtx) return COLOR_TX_STATUS_OFFLINE; case TransactionStatus::Unconfirmed: return QIcon(":/icons/transaction_0"); + case TransactionStatus::Abandoned: + return QIcon(":/icons/transaction_abandoned"); case TransactionStatus::Confirming: switch(wtx->status.depth) { @@ -573,6 +578,11 @@ QVariant TransactionTableModel::data(const QModelIndex &index, int role) const case Qt::TextAlignmentRole: return column_alignments[index.column()]; case Qt::ForegroundRole: + // Use the "danger" color for abandoned transactions + if(rec->status.status == TransactionStatus::Abandoned) + { + return COLOR_TX_STATUS_DANGER; + } // Non-confirmed (but not immature) as transactions are grey if(!rec->status.countsForBalance && rec->status.status != TransactionStatus::Immature) { diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index a4d4c7a35..a352228c3 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -37,7 +37,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *parent) : QWidget(parent), model(0), transactionProxyModel(0), - transactionView(0) + transactionView(0), abandonAction(0) { // Build filter row setContentsMargins(0,0,0,0); @@ -137,6 +137,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa transactionView = view; // Actions + abandonAction = new QAction(tr("Abandon transaction"), this); QAction *copyAddressAction = new QAction(tr("Copy address"), this); QAction *copyLabelAction = new QAction(tr("Copy label"), this); QAction *copyAmountAction = new QAction(tr("Copy amount"), this); @@ -153,8 +154,10 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa contextMenu->addAction(copyTxIDAction); contextMenu->addAction(copyTxHexAction); contextMenu->addAction(copyTxPlainText); - contextMenu->addAction(editLabelAction); contextMenu->addAction(showDetailsAction); + contextMenu->addSeparator(); + contextMenu->addAction(abandonAction); + contextMenu->addAction(editLabelAction); mapperThirdPartyTxUrls = new QSignalMapper(this); @@ -170,6 +173,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa connect(view, SIGNAL(doubleClicked(QModelIndex)), this, SIGNAL(doubleClicked(QModelIndex))); connect(view, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextualMenu(QPoint))); + connect(abandonAction, SIGNAL(triggered()), this, SLOT(abandonTx())); connect(copyAddressAction, SIGNAL(triggered()), this, SLOT(copyAddress())); connect(copyLabelAction, SIGNAL(triggered()), this, SLOT(copyLabel())); connect(copyAmountAction, SIGNAL(triggered()), this, SLOT(copyAmount())); @@ -360,12 +364,37 @@ void TransactionView::exportClicked() void TransactionView::contextualMenu(const QPoint &point) { QModelIndex index = transactionView->indexAt(point); + QModelIndexList selection = transactionView->selectionModel()->selectedRows(0); + + // check if transaction can be abandoned, disable context menu action in case it doesn't + uint256 hash; + hash.SetHex(selection.at(0).data(TransactionTableModel::TxHashRole).toString().toStdString()); + abandonAction->setEnabled(model->transactionCanBeAbandoned(hash)); + if(index.isValid()) { contextMenu->exec(QCursor::pos()); } } +void TransactionView::abandonTx() +{ + if(!transactionView || !transactionView->selectionModel()) + return; + QModelIndexList selection = transactionView->selectionModel()->selectedRows(0); + + // get the hash from the TxHashRole (QVariant / QString) + uint256 hash; + QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString(); + hash.SetHex(hashQStr.toStdString()); + + // Abandon the wallet transaction over the walletModel + model->abandonTransaction(hash); + + // Update the table + model->getTransactionTableModel()->updateTransaction(hashQStr, CT_UPDATED, false); +} + void TransactionView::copyAddress() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::AddressRole); diff --git a/src/qt/transactionview.h b/src/qt/transactionview.h index 2cfbd471b..e9b9d5b6b 100644 --- a/src/qt/transactionview.h +++ b/src/qt/transactionview.h @@ -75,6 +75,7 @@ private: QFrame *dateRangeWidget; QDateTimeEdit *dateFrom; QDateTimeEdit *dateTo; + QAction *abandonAction; QWidget *createDateRangeWidget(); @@ -97,6 +98,7 @@ private Q_SLOTS: void copyTxPlainText(); void openThirdPartyTxUrl(QString url); void updateWatchOnlyColumn(bool fHaveWatchOnly); + void abandonTx(); Q_SIGNALS: void doubleClicked(const QModelIndex&); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index cf38c64eb..ce230d6ae 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -668,3 +668,18 @@ bool WalletModel::saveReceiveRequest(const std::string &sAddress, const int64_t else return wallet->AddDestData(dest, key, sRequest); } + +bool WalletModel::transactionCanBeAbandoned(uint256 hash) const +{ + LOCK2(cs_main, wallet->cs_wallet); + const CWalletTx *wtx = wallet->GetWalletTx(hash); + if (!wtx || wtx->isAbandoned() || wtx->GetDepthInMainChain() > 0 || wtx->InMempool()) + return false; + return true; +} + +bool WalletModel::abandonTransaction(uint256 hash) const +{ + LOCK2(cs_main, wallet->cs_wallet); + return wallet->AbandonTransaction(hash); +} diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 7a47eda86..e5470bf61 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -200,6 +200,9 @@ public: void loadReceiveRequests(std::vector& vReceiveRequests); bool saveReceiveRequest(const std::string &sAddress, const int64_t nId, const std::string &sRequest); + bool transactionCanBeAbandoned(uint256 hash) const; + bool abandonTransaction(uint256 hash) const; + private: CWallet *wallet; bool fHaveWatchOnly;