diff --git a/src/app.cpp b/src/app.cpp index 33ae2e0..6dc15e3 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -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 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 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 successfulOpids; - for (const auto& [opid, txid] : parsed.successTxidsByOpid) { - successfulOpids.insert(opid); - markPendingSendTransactionSucceeded(opid, txid); - send_txids_.insert(txid); - } std::vector successOpids; std::vector failedOrStaleOpids; for (const auto& opid : terminalOpids) { diff --git a/src/app.h b/src/app.h index 21fc7f9..ff677d6 100644 --- a/src/app.h +++ b/src/app.h @@ -250,11 +250,18 @@ public: // Wallet backup void backupWallet(const std::string& destination, std::function callback); - // Transaction operations - void sendTransaction(const std::string& from, const std::string& to, + // Transaction operations + void sendTransaction(const std::string& from, const std::string& to, double amount, double fee, const std::string& memo, std::function callback); + // Register a daemon async operation id (z_shieldcoinbase / z_mergetoaddress / + // auto-shield) with the shared opid poller so its eventual success/failure is + // surfaced and balances/transactions refresh on completion. z_sendmany uses the + // richer pending-send path internally; this is for operations with no optimistic + // transaction row of their own. + void trackOperation(const std::string& opid); + // Force refresh void refreshNow(); void refreshMiningInfo(); @@ -399,6 +406,10 @@ private: const std::string& txid); void removePendingSendTransactions(const std::vector& opids, bool restoreBalances); + // Deliver a deferred z_sendmany result to its waiting UI callback once the opid + // reaches a terminal status. Returns true if a callback was registered (and fired). + bool invokeSendResultCallback(const std::string& opid, bool ok, + const std::string& result); void applyPendingSendBalanceDeltas(bool includeAggregateBalances); std::string transactionHistoryCacheWalletIdentity() const; bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity); @@ -576,6 +587,10 @@ private: std::int64_t timestamp = 0; }; std::unordered_map pending_send_info_; + // z_sendmany UI callbacks held until the opid reaches a terminal status, so the + // user isn't told "sent successfully" before the tx is actually built/broadcast. + std::unordered_map> + pending_send_callbacks_; // Txids from completed z_sendmany operations. // Ensures shielded sends are discoverable by z_viewtransaction // even when they don't appear in listtransactions or diff --git a/src/app_network.cpp b/src/app_network.cpp index 8c60c8b..0c3ec53 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -730,6 +730,27 @@ void App::removePendingSendTransactions(const std::vector& 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().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); }; }); } diff --git a/src/services/network_refresh_service.cpp b/src/services/network_refresh_service.cpp index 2d243a0..72ada1a 100644 --- a/src/services/network_refresh_service.cpp +++ b/src/services/network_refresh_service.cpp @@ -1095,6 +1095,7 @@ NetworkRefreshService::OperationStatusPollResult NetworkRefreshService::parseOpe } catch (...) {} } parsed.failureMessages.push_back(msg); + parsed.failureByOpid[opid] = msg; } } diff --git a/src/services/network_refresh_service.h b/src/services/network_refresh_service.h index a8f57ff..96bb946 100644 --- a/src/services/network_refresh_service.h +++ b/src/services/network_refresh_service.h @@ -198,6 +198,7 @@ public: std::vector successTxids; std::unordered_map successTxidsByOpid; std::vector failureMessages; + std::unordered_map failureByOpid; // opid -> error message bool anySuccess = false; }; diff --git a/src/ui/windows/shield_dialog.cpp b/src/ui/windows/shield_dialog.cpp index 003bb19..83eed21 100644 --- a/src/ui/windows/shield_dialog.cpp +++ b/src/ui/windows/shield_dialog.cpp @@ -181,7 +181,7 @@ void ShieldDialog::render(App* app) double fee = s_fee; int limit = s_utxo_limit; if (app->worker()) { - app->worker()->post([rpc = app->rpc(), from, to, fee, limit]() -> rpc::RPCWorker::MainCb { + app->worker()->post([app, rpc = app->rpc(), from, to, fee, limit]() -> rpc::RPCWorker::MainCb { nlohmann::json result; std::string error; try { @@ -190,12 +190,15 @@ void ShieldDialog::render(App* app) } catch (const std::exception& e) { error = e.what(); } - return [result, error]() { + return [app, result, error]() { s_operation_pending = false; if (error.empty()) { s_operation_id = result.value("opid", ""); s_status_message = "Operation submitted: " + s_operation_id; Notifications::instance().success(TR("shield_started")); + // Register with the shared poller so an async failure is + // surfaced (and balances refresh) even after this dialog closes. + app->trackOperation(s_operation_id); } else { s_status_message = "Error: " + error; Notifications::instance().error("Shield failed: " + error); @@ -210,7 +213,7 @@ void ShieldDialog::render(App* app) double fee = s_fee; int limit = s_utxo_limit; if (app->worker()) { - app->worker()->post([rpc = app->rpc(), fromAddrs, to, fee, limit]() -> rpc::RPCWorker::MainCb { + app->worker()->post([app, rpc = app->rpc(), fromAddrs, to, fee, limit]() -> rpc::RPCWorker::MainCb { nlohmann::json addrs = nlohmann::json::array(); for (const auto& addr : fromAddrs) addrs.push_back(addr); nlohmann::json result; @@ -221,12 +224,15 @@ void ShieldDialog::render(App* app) } catch (const std::exception& e) { error = e.what(); } - return [result, error]() { + return [app, result, error]() { s_operation_pending = false; if (error.empty()) { s_operation_id = result.value("opid", ""); s_status_message = "Operation submitted: " + s_operation_id; Notifications::instance().success(TR("merge_started")); + // Register with the shared poller so an async failure is + // surfaced (and balances refresh) even after this dialog closes. + app->trackOperation(s_operation_id); } else { s_status_message = "Error: " + error; Notifications::instance().error("Merge failed: " + error);