diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 884e8e8..bc1ebab 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -90,6 +90,7 @@ TransactionView::TransactionView(QWidget *parent) : QAction *copyTxIDAction = new QAction(QIcon(":/menu_16/copy"), tr("Copy transaction ID"), this); QAction *editLabelAction = new QAction(QIcon(":/menu_16/edit"), tr("Edit label"), this); QAction *showDetailsAction = new QAction(QIcon(":/menu_16/search"), tr("Show transaction details"), this); + abandonAction = new QAction(QIcon(":/menu_16/remove"), tr("Abandon transaction"), this); contextMenu = new QMenu(); contextMenu->addAction(copyAddressAction); @@ -98,6 +99,8 @@ TransactionView::TransactionView(QWidget *parent) : contextMenu->addAction(copyTxIDAction); contextMenu->addAction(editLabelAction); contextMenu->addAction(showDetailsAction); + contextMenu->addSeparator(); + contextMenu->addAction(abandonAction); contextMenu->setStyleSheet("QMenu {\ background-color: #000; \ border: 1px solid #f26522;\ @@ -129,6 +132,7 @@ TransactionView::TransactionView(QWidget *parent) : connect(copyTxIDAction, SIGNAL(triggered()), this, SLOT(copyTxID())); connect(editLabelAction, SIGNAL(triggered()), this, SLOT(editLabel())); connect(showDetailsAction, SIGNAL(triggered()), this, SLOT(showDetails())); + connect(abandonAction, SIGNAL(triggered()), this, SLOT(abandonTransaction())); connect(view->horizontalHeader(), SIGNAL(sectionClicked(int)), this, SLOT(headerCol0Clicked(int))); } @@ -310,6 +314,17 @@ void TransactionView::contextualMenu(const QPoint &point) QModelIndex index = transactionView->indexAt(point); if(index.isValid()) { + // Only enable "Abandon transaction" for unconfirmed / conflicted txs + QModelIndexList selection = transactionView->selectionModel()->selectedRows(); + bool fCanAbandon = false; + if (!selection.isEmpty()) { + int status = selection.at(0).data(TransactionTableModel::StatusRole).toInt(); + fCanAbandon = (status == TransactionStatus::Unconfirmed || + status == TransactionStatus::Conflicted || + status == TransactionStatus::Offline); + } + abandonAction->setEnabled(fCanAbandon); + contextMenu->exec(QCursor::pos()); } } @@ -392,6 +407,37 @@ void TransactionView::showDetails() } } +void TransactionView::abandonTransaction() +{ + if(!transactionView->selectionModel() || !model) + return; + QModelIndexList selection = transactionView->selectionModel()->selectedRows(); + if(selection.isEmpty()) + return; + + QString hash = selection.at(0).data(TransactionTableModel::TxIDRole).toString(); + if(hash.isEmpty()) + return; + + // Confirm with the user + QMessageBox::StandardButton reply = QMessageBox::question( + this, tr("Abandon transaction"), + tr("Abandon transaction %1?\n\nThis will mark the transaction as abandoned and free its inputs for re-spending. Use this only for stuck or conflicted transactions that will never confirm.").arg(hash), + QMessageBox::Yes | QMessageBox::No); + if(reply != QMessageBox::Yes) + return; + + if(!model->abandonTransaction(hash)) + { + QMessageBox::warning(this, tr("Abandon transaction"), + tr("Failed to abandon transaction. It may already be confirmed, or it does not belong to this wallet.")); + return; + } + + // Refresh the transactions table + model->getTransactionTableModel()->refresh(); +} + QWidget *TransactionView::createDateRangeWidget() { dateRangeWidget = new QFrame(); diff --git a/src/qt/transactionview.h b/src/qt/transactionview.h index 1c20120..2774cc0 100644 --- a/src/qt/transactionview.h +++ b/src/qt/transactionview.h @@ -61,6 +61,7 @@ private: QLineEdit *amountWidget; QMenu *contextMenu; + QAction *abandonAction; QFrame *dateRangeWidget; QDateTimeEdit *dateFrom; @@ -72,6 +73,7 @@ private slots: void contextualMenu(const QPoint &); void dateRangeChanged(); void showDetails(); + void abandonTransaction(); void copyAddress(); void editLabel(); void copyLabel(); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 7054532..586ef05 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -453,6 +453,15 @@ AddressTableModel *WalletModel::getAddressTableModel() return addressTableModel; } +bool WalletModel::abandonTransaction(const QString &hash) +{ + if (!wallet) + return false; + uint256 txHash; + txHash.SetHex(hash.toStdString()); + return wallet->AbandonTransaction(txHash); +} + TransactionTableModel *WalletModel::getTransactionTableModel() { return transactionTableModel; diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index ac6bf59..533264a 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -68,6 +68,7 @@ public: OptionsModel *getOptionsModel(); AddressTableModel *getAddressTableModel(); TransactionTableModel *getTransactionTableModel(); + bool abandonTransaction(const QString &hash); qint64 getBalance() const; qint64 getStake() const; diff --git a/src/rpcwallet.cpp b/src/rpcwallet.cpp index 1e41ba2..a10e7f2 100644 --- a/src/rpcwallet.cpp +++ b/src/rpcwallet.cpp @@ -1822,6 +1822,24 @@ Value repairwallet(const Array& params, bool fHelp) return result; } +// triangles: mark an in-wallet transaction as abandoned +Value abandontransaction(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 1) + throw runtime_error( + "abandontransaction \"txid\"\n" + " is the transaction ID of the wallet transaction to abandon.\n" + "Mark an in-wallet transaction as abandoned. This frees its inputs so they can be re-spent.\n" + "Only unconfirmed transactions that belong to this wallet can be abandoned."); + + uint256 hash; + hash.SetHex(params[0].get_str()); + if (!pwalletMain->AbandonTransaction(hash)) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction not eligible for abandonment"); + + return Value::null; +} + // triangles: resend unconfirmed wallet transactions Value resendtx(const Array& params, bool fHelp) { diff --git a/src/trianglesrpc.cpp b/src/trianglesrpc.cpp index fd046a7..b6e04f1 100644 --- a/src/trianglesrpc.cpp +++ b/src/trianglesrpc.cpp @@ -332,6 +332,7 @@ static const CRPCCommand vRPCCommands[] = { "checkwallet", &checkwallet, false, true}, { "repairwallet", &repairwallet, false, true}, { "resendtx", &resendtx, false, true}, + { "abandontransaction", &abandontransaction, true, true}, { "makekeypair", &makekeypair, false, true}, { "smsgenable", &smsgenable, false, false}, diff --git a/src/trianglesrpc.h b/src/trianglesrpc.h index b8e0241..c057d41 100644 --- a/src/trianglesrpc.h +++ b/src/trianglesrpc.h @@ -200,6 +200,7 @@ extern json_spirit::Value reservebalance(const json_spirit::Array& params, bool extern json_spirit::Value checkwallet(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value repairwallet(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value resendtx(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value abandontransaction(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value makekeypair(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value validatepubkey(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value getnewpubkey(const json_spirit::Array& params, bool fHelp); diff --git a/src/wallet.cpp b/src/wallet.cpp index 2b7d0a8..5598416 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -733,6 +733,56 @@ bool CWallet::EraseFromWallet(uint256 hash) return true; } +// triangles: mark an in-wallet transaction as abandoned, freeing its inputs +// for re-spending. Use for stuck or conflicted transactions that will never +// confirm. Returns false if the transaction is not eligible (already +// confirmed, not in this wallet, or not from us). +bool CWallet::AbandonTransaction(const uint256& hashTx) +{ + LOCK2(cs_main, cs_wallet); + + if (!mapWallet.count(hashTx)) + return false; + + CWalletTx& wtx = mapWallet[hashTx]; + + // Cannot abandon a transaction that is already in the main chain + if (wtx.GetDepthInMainChain() > 0) + return false; + + // Only allow abandoning transactions that involve this wallet + if (!wtx.IsFromMe()) + return false; + + // Find descendant wallet txs (those spending this tx's outputs) so the + // caller can refresh the UI. The descendants are not modified here; they + // will simply stop being marked as having a valid parent. + std::set sDescendants; + for (unsigned int i = 0; i < wtx.vout.size(); i++) { + if (!IsMine(wtx.vout[i])) + continue; + for (std::map::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it) { + CWalletTx& candidate = it->second; + if (candidate.GetHash() == hashTx) + continue; + for (const CTxIn& txin : candidate.vin) { + if (txin.prevout.hash == hashTx && txin.prevout.n == i) { + sDescendants.insert(candidate.GetHash()); + break; + } + } + } + } + + // Erase the original tx from the wallet and the wallet DB. This releases + // the inputs (vfSpent was tracked on the wtx) and resolves the conflict. + bool fErased = EraseFromWallet(hashTx); + + LogPrintf("CWallet::AbandonTransaction: %s abandoned (%u descendant(s) noted)\n", + hashTx.ToString().c_str(), sDescendants.size()); + return fErased; +} + bool CWallet::IsMine(const CTxIn &txin) const { diff --git a/src/wallet.h b/src/wallet.h index 6a1c45a..b3567a7 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -192,6 +192,7 @@ public: bool AddToWallet(const CWalletTx& wtxIn); bool AddToWalletIfInvolvingMe(const CTransaction& tx, const CBlock* pblock, bool fUpdate = false, bool fFindBlock = false); bool EraseFromWallet(uint256 hash); + bool AbandonTransaction(const uint256& hashTx); void WalletUpdateSpent(const CTransaction& prevout, bool fBlock = false); int ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate = false); bool ScanForWalletTransactionsFromIndex(CBlockIndex* pindexStart, bool fUpdate, int* pnFound = nullptr);