diff --git a/src/ui/windows/console_command_executor.cpp b/src/ui/windows/console_command_executor.cpp index 47238ec..ca651c1 100644 --- a/src/ui/windows/console_command_executor.cpp +++ b/src/ui/windows/console_command_executor.cpp @@ -48,7 +48,13 @@ void FullNodeConsoleExecutor::submit(const std::string& cmd) } auto call = BuildConsoleRpcCall(cmd); - if (!call.valid) return; + if (!call.valid) { + if (!call.error.empty()) { + std::lock_guard lk(results_mutex_); + results_.push_back({call.error, true}); + } + return; + } const std::string method = call.method; const nlohmann::json params = call.params; diff --git a/src/ui/windows/console_input_model.cpp b/src/ui/windows/console_input_model.cpp index 2d7d923..983c596 100644 --- a/src/ui/windows/console_input_model.cpp +++ b/src/ui/windows/console_input_model.cpp @@ -170,6 +170,10 @@ ConsoleRpcCall BuildConsoleRpcCall(const std::string& command) call.params.push_back(parsed); continue; } + // Malformed JSON — fail the whole command instead of silently sending it as a string. + call.valid = false; + call.error = "Argument " + std::to_string(argIndex) + " is not valid JSON: " + arg; + return call; } if (arg == "true") { diff --git a/src/ui/windows/console_input_model.h b/src/ui/windows/console_input_model.h index 102a435..4b606a2 100644 --- a/src/ui/windows/console_input_model.h +++ b/src/ui/windows/console_input_model.h @@ -17,6 +17,7 @@ struct ConsoleRpcCall { bool valid = false; std::string method; nlohmann::json params = nlohmann::json::array(); + std::string error; // set (with valid=false) when an argument fails to parse }; enum class ConsoleResultLineRole { diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index 7e96ce7..afba9cf 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -794,9 +794,20 @@ void RenderSendConfirmPopup(App* app) { Type().text(TypeStyle::Body2, TR("sending")); } else { if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) { + // Re-validate against LIVE state — the confirm dialog persists across frames, so the + // balance could have dropped or sync (re)started (or the fee bumped total over available) + // since Review. Don't broadcast a now-invalid transaction. + const auto& liveState = app->getWalletState(); + double liveAvail = GetAvailableBalance(app); + if (!app->isConnected() || liveState.sync.syncing || s_amount <= 0 || + (s_amount + s_fee) > liveAvail) { + s_tx_status = "Cannot send now — check connection, sync, and available balance."; + s_status_success = false; + s_status_timestamp = ImGui::GetTime(); + } // A locked encrypted lite wallet can't spend; prompt to unlock instead of sending // (the backend would otherwise reject with "Wallet is locked"). - if (app->liteWallet() && app->getWalletState().isLocked()) { + else if (app->liteWallet() && app->getWalletState().isLocked()) { app->requestLiteUnlock(); } else { s_sending = true;