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:
@@ -730,6 +730,27 @@ void App::removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||
state_.last_tx_update = std::time(nullptr);
|
||||
}
|
||||
|
||||
void App::trackOperation(const std::string& opid)
|
||||
{
|
||||
if (opid.empty()) return;
|
||||
// Touched only from the main thread (sendTransaction's MainCb and the opid poller's
|
||||
// MainCb both run via drainResults()), so no locking is needed.
|
||||
if (std::find(pending_opids_.begin(), pending_opids_.end(), opid) != pending_opids_.end())
|
||||
return;
|
||||
pending_opids_.push_back(opid);
|
||||
}
|
||||
|
||||
bool App::invokeSendResultCallback(const std::string& opid, bool ok,
|
||||
const std::string& result)
|
||||
{
|
||||
auto it = pending_send_callbacks_.find(opid);
|
||||
if (it == pending_send_callbacks_.end()) return false;
|
||||
auto cb = std::move(it->second);
|
||||
pending_send_callbacks_.erase(it);
|
||||
if (cb) cb(ok, result);
|
||||
return true;
|
||||
}
|
||||
|
||||
void App::applyPendingSendBalanceDeltas(bool includeAggregateBalances)
|
||||
{
|
||||
for (const auto& [opid, pending] : pending_send_info_) {
|
||||
@@ -1024,21 +1045,39 @@ void App::refreshCoreData()
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!targetZAddr.empty() && rpc_) {
|
||||
if (!targetZAddr.empty() && worker_) {
|
||||
DEBUG_LOGF("[AutoShield] Shielding %.8f DRGX to %s\n",
|
||||
state_.transparent_balance, targetZAddr.c_str());
|
||||
rpc_->z_shieldCoinbase("*", targetZAddr, 0.0001, 50,
|
||||
[this](const json& result) {
|
||||
if (result.contains("opid")) {
|
||||
DEBUG_LOGF("[AutoShield] Started: %s\n",
|
||||
result["opid"].get<std::string>().c_str());
|
||||
// Use the user-configured fee, formatted fixed-decimal so the daemon's
|
||||
// ParseFixedPoint accepts it (a small double would serialize to "5e-05").
|
||||
const std::string feeStr =
|
||||
util::formatAmountFixed(settings_ ? settings_->getDefaultFee() : 0.0001);
|
||||
// This callback runs on the UI thread (drainResults). Build/broadcast
|
||||
// on the worker thread — never block the UI with synchronous RPC.
|
||||
worker_->post([this, targetZAddr, feeStr]() -> rpc::RPCWorker::MainCb {
|
||||
std::string opid;
|
||||
std::string err;
|
||||
try {
|
||||
rpc::RPCClient::TraceScope trace("Auto-shield / z_shieldcoinbase");
|
||||
auto result = rpc_->call("z_shieldcoinbase",
|
||||
{std::string("*"), targetZAddr, feeStr, 50});
|
||||
opid = result.value("opid", "");
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
}
|
||||
return [this, opid, err]() {
|
||||
auto_shield_pending_ = false;
|
||||
if (!err.empty()) {
|
||||
DEBUG_LOGF("[AutoShield] Error: %s\n", err.c_str());
|
||||
return;
|
||||
}
|
||||
auto_shield_pending_ = false;
|
||||
},
|
||||
[this](const std::string& err) {
|
||||
DEBUG_LOGF("[AutoShield] Error: %s\n", err.c_str());
|
||||
auto_shield_pending_ = false;
|
||||
});
|
||||
if (!opid.empty()) {
|
||||
DEBUG_LOGF("[AutoShield] Started: %s\n", opid.c_str());
|
||||
// Surface the async result + refresh balances on completion.
|
||||
trackOperation(opid);
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
auto_shield_pending_ = false;
|
||||
}
|
||||
@@ -2070,15 +2109,21 @@ void App::sendTransaction(const std::string& from, const std::string& to,
|
||||
transactions_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
network_refresh_.markWalletMutationRefresh();
|
||||
// Track the opid so we can poll for completion
|
||||
// z_sendmany only returned an opid: the transaction is built/signed/
|
||||
// broadcast asynchronously by the daemon. Defer the user-facing
|
||||
// success/failure to the opid poller (app.cpp) so we don't report
|
||||
// "sent successfully" for an operation that may still fail.
|
||||
if (!result_str.empty()) {
|
||||
pending_opids_.push_back(result_str);
|
||||
upsertPendingSendTransaction(result_str, from, to, amount, memo);
|
||||
if (callback) pending_send_callbacks_[result_str] = callback;
|
||||
} else if (callback) {
|
||||
callback(true, result_str); // no opid to track — report as-is
|
||||
}
|
||||
} else {
|
||||
send_progress_active_ = false;
|
||||
if (callback) callback(false, result_str);
|
||||
}
|
||||
if (callback) callback(ok, result_str);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user