Repository navigation
fix(qt): surface a mempool rejection from WalletModel::sendCoins #7905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -324,7 +324,20 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact | |
| return SendCoinsReturn(OK); | ||
| } | ||
|
|
||
| void WalletModel::sendCoins(WalletModelTransaction& transaction, bool fIsCoinJoin) | ||
| namespace { | ||
| bool WasSent(interfaces::Wallet& wallet, const uint256& txid) | ||
| { | ||
| interfaces::WalletTxStatus status{}; | ||
| interfaces::WalletOrderForm order_form; | ||
| bool in_mempool{false}; | ||
| int num_blocks{0}; | ||
| wallet.getWalletTxDetails(txid, status, order_form, in_mempool, num_blocks); | ||
| return status.depth_in_main_chain > 0 || | ||
| (status.depth_in_main_chain == 0 && (in_mempool || status.is_islocked)); | ||
| } | ||
| } // namespace | ||
|
|
||
| WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction& transaction, bool fIsCoinJoin) | ||
| { | ||
| QByteArray transaction_array; /* store serialized transaction */ | ||
|
|
||
|
|
@@ -342,7 +355,18 @@ void WalletModel::sendCoins(WalletModelTransaction& transaction, bool fIsCoinJoi | |
| } | ||
|
|
||
| auto& newTx = transaction.getWtx(); | ||
| wallet().commitTransaction(newTx, /*value_map=*/std::move(mapValue), std::move(vOrderForm)); | ||
| // A refused transaction was never relayed. Abandon it, so the wallet | ||
| // does not rebroadcast a payment the user is told failed and its inputs | ||
| // are free for a retry. It cannot be abandoned if a resend got it into | ||
| // the mempool or a block, or it was InstantSend-locked, meanwhile, and | ||
| // then it was sent, or if a spend of the same inputs has confirmed, and | ||
| // then it was not. | ||
| const auto res{wallet().commitTransaction(newTx, /*value_map=*/std::move(mapValue), std::move(vOrderForm))}; | ||
| if (!res && (wallet().abandonTransaction(newTx->GetHash()) || !WasSent(wallet(), newTx->GetHash()))) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving this as is. Abandoning a refused send leaves the AVOID_REUSE "used" mark on its input addresses, but that is how abandoning already works in develop and upstream Bitcoin Core: 🤖 Posted autonomously by Claude on behalf of pasta. |
||
| const bilingual_str error{util::ErrorString(res)}; | ||
| return SendCoinsReturn(TransactionCommitFailed, | ||
| QString::fromStdString(error.translated.empty() ? error.original : error.translated)); | ||
| } | ||
|
|
||
| CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); | ||
| ssTx << *newTx; | ||
|
|
@@ -375,6 +399,7 @@ void WalletModel::sendCoins(WalletModelTransaction& transaction, bool fIsCoinJoi | |
| } | ||
|
|
||
| checkBalanceChanged(m_wallet->getBalances()); // update balance immediately, otherwise there could be a short noticeable delay until pollBalanceChanged hits | ||
| return SendCoinsReturn(OK); | ||
| } | ||
|
|
||
| OptionsModel* WalletModel::getOptionsModel() const | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a rejected send is retried and transaction creation produces the same txid (for example, a coin-controlled subtract-fee/no-change payment at the same tip), the first pass has left that wallet entry abandoned. This call invokes
CommitTransaction(), whoseAddToWallet(..., TxStateInactive{})sees the existing abandonedTxStateInactiveas the same variant and hits its fatal serialized-index assertion (-1versus0); without assertions it remains abandoned and cannot be relayed. Thus the retained-form retry can crashdash-qtor repeatedly fail; reactivate the abandoned state before recommitting and cover an actual identical retry.AGENTS.md reference: AGENTS.md:L40-L44
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reproduced and fixed in c8e27b5. If the wallet already holds the transaction as abandoned,
CWallet::CommitTransactionnow un-abandons it before re-adding it, and the existing reconcile step marks its inputs spent again. A newTestGUIblock sends one prepared transaction twice, refused and then accepted; without the wallet change it hits theAddToWalletassertion. (Develop already crashed the same way after a manual "Abandon transaction" followed by an identical resend.)🤖 Posted autonomously by Claude on behalf of pasta.