fix(tx): track async operations to completion (send/shield/auto-shield)

z_sendmany returns an opid immediately; the tx is built/signed/broadcast
asynchronously afterward. The send path showed "Transaction sent successfully!"
and cleared the form on opid receipt, so a later async failure contradicted it.
Shield/merge stored the opid only in a dialog-local static (never polled), and
auto-shield ran a blocking z_shieldcoinbase on the UI thread and discarded its
opid — async failures of all three were silently lost.

- Add App::trackOperation(opid) so shield/merge/auto-shield register with the
  shared opid poller (failures surface, balances refresh on completion).
- Defer the full-node send's success/failure to the poller via per-opid callbacks
  (parseOperationStatusPoll now exposes failureByOpid); the "Sending..." spinner
  covers the finalizing window, and the form is kept until terminal status.
- Dispatch auto-shield through the worker thread and use the configured fee.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:16:48 -05:00
parent 20b22410e9
commit 1bc7f5c8cd
6 changed files with 113 additions and 28 deletions

View File

@@ -739,9 +739,25 @@ void App::update()
auto parsed = services::NetworkRefreshService::parseOperationStatusPoll(result, opids);
return [this, parsed = std::move(parsed)]() mutable {
opid_poll_in_progress_ = false;
for (const auto& msg : parsed.failureMessages) {
ui::Notifications::instance().error(msg);
// Successes: hand the real txid to any waiting send UI callback.
std::unordered_set<std::string> successfulOpids;
for (const auto& [opid, txid] : parsed.successTxidsByOpid) {
successfulOpids.insert(opid);
markPendingSendTransactionSucceeded(opid, txid);
send_txids_.insert(txid);
invokeSendResultCallback(opid, true, txid);
}
// Failures: route to the originating send UI when there is one (it shows
// its own error toast); otherwise surface a generic notification (this is
// how shield/merge/auto-shield failures become visible).
for (const auto& [opid, msg] : parsed.failureByOpid) {
if (!invokeSendResultCallback(opid, false, msg)) {
ui::Notifications::instance().error(msg);
}
}
std::vector<std::string> terminalOpids = std::move(parsed.doneOpids);
terminalOpids.insert(terminalOpids.end(),
parsed.staleOpids.begin(), parsed.staleOpids.end());
@@ -750,13 +766,14 @@ void App::update()
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
pending_opids_.end());
}
// Stale opids (no longer reported by the daemon): let any waiting send UI
// know the outcome couldn't be confirmed rather than spinning forever.
for (const auto& opid : parsed.staleOpids) {
invokeSendResultCallback(opid, false, TR("send_status_unconfirmed"));
}
if (parsed.anySuccess) {
std::unordered_set<std::string> successfulOpids;
for (const auto& [opid, txid] : parsed.successTxidsByOpid) {
successfulOpids.insert(opid);
markPendingSendTransactionSucceeded(opid, txid);
send_txids_.insert(txid);
}
std::vector<std::string> successOpids;
std::vector<std::string> failedOrStaleOpids;
for (const auto& opid : terminalOpids) {