Files
ObsidianDragon/src/daemon/daemon_controller.h
DanS 37c8287a12 feat(fullnode): add "Repair Wallet" (-zapwallettxes=2) to Settings
When a note's stored record is corrupt or its tx isn't in the canonical chain,
z_sendmany fails to build a valid sapling spend proof even after a full -rescan,
because a plain rescan replays witnesses but keeps the existing tx/note records.
The zcashd repair for this is -zapwallettxes=2, which deletes all wallet tx/note
records and rebuilds them from the chain (keys/addresses preserved).

Adds a RepairWallet lifecycle operation that mirrors the existing -rescan plumbing
(one-shot zapOnNextStart flag on the embedded daemon; -zapwallettxes=2 implies and
supersedes -rescan), an App::repairWallet() that reuses the rescan status UI (so the
status bar + warmup-end completion detection apply), and a confirmed "Repair Wallet"
button + dialog in Settings → node maintenance (embedded daemon only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:42:10 -05:00

239 lines
9.1 KiB
C++

#pragma once
#include "embedded_daemon.h"
#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
namespace dragonx {
namespace config { class Settings; }
namespace daemon {
class DaemonController {
public:
using State = EmbeddedDaemon::State;
using StateCallback = EmbeddedDaemon::StateCallback;
enum class ShutdownAction {
DisconnectOnly,
StopDaemon
};
struct ShutdownDecision {
ShutdownAction action = ShutdownAction::DisconnectOnly;
const char* logReason = "no embedded daemon";
const char* status = "Disconnecting...";
};
enum class LifecycleOperation {
ManualRestart,
Rescan,
RepairWallet, // restart with -zapwallettxes=2 (wipe & rebuild wallet tx records)
DeleteBlockchainData,
BootstrapStop
};
struct LifecycleDecision {
LifecycleOperation operation = LifecycleOperation::ManualRestart;
bool allowed = false;
bool wasRunning = false;
const char* taskName = "";
const char* status = "";
const char* warning = "";
bool resetCrashCount = false;
bool setRescanOnNextStart = false;
bool disconnectRpc = false;
int restartDelayMs = 0;
bool setZapOnNextStart = false;
};
class LifecycleTaskContext {
public:
virtual ~LifecycleTaskContext() = default;
virtual bool cancelled() const = 0;
virtual bool shuttingDown() const = 0;
virtual void sleepForMs(int milliseconds) = 0;
};
class LifecycleRuntime {
public:
virtual ~LifecycleRuntime() = default;
virtual void stopDaemonWithPolicy() = 0;
virtual bool startDaemon() = 0;
virtual int deleteBlockchainData() = 0;
virtual void resetOutputOffset() = 0;
virtual void requestRpcStopAndDisconnect(const char* context, const char* reason) = 0;
};
struct LifecycleExecutionResult {
bool completed = false;
bool cancelled = false;
bool stopped = false;
bool started = false;
int deletedItems = 0;
};
DaemonController();
~DaemonController();
DaemonController(const DaemonController&) = delete;
DaemonController& operator=(const DaemonController&) = delete;
EmbeddedDaemon* daemon() { return daemon_.get(); }
const EmbeddedDaemon* daemon() const { return daemon_.get(); }
void setStateCallback(StateCallback callback);
void syncSettings(const config::Settings* settings);
bool start(const config::Settings* settings);
void stop(int waitMs);
bool isRunning() const;
bool externalDaemonDetected() const;
State state() const;
const std::string& lastError() const;
int crashCount() const;
int lastBlockHeight() const;
double memoryUsageMB() const;
std::vector<std::string> recentLines(std::size_t count) const;
std::string outputSince(std::size_t& offset) const;
void resetCrashCount();
void setRescanOnNextStart(bool enabled);
bool rescanOnNextStart() const;
void setZapOnNextStart(bool enabled);
bool zapOnNextStart() const;
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
bool externalDaemonDetected,
bool keepDaemonRunning,
bool stopExternalDaemon) {
if (!hasDaemon) {
return {};
}
if (keepDaemonRunning) {
return {ShutdownAction::DisconnectOnly,
"keep_daemon_running enabled",
"Disconnecting (daemon stays running)..."};
}
if (externalDaemonDetected && !stopExternalDaemon) {
return {ShutdownAction::DisconnectOnly,
"external daemon (not ours to stop)",
"Disconnecting (daemon stays running)..."};
}
return {ShutdownAction::StopDaemon,
"stopping managed daemon",
"Sending stop command to daemon..."};
}
static LifecycleDecision evaluateLifecycleOperation(LifecycleOperation operation,
bool usingEmbeddedDaemon,
bool hasDaemon,
bool daemonRunning,
bool restartInProgress = false) {
switch (operation) {
case LifecycleOperation::ManualRestart:
if (!usingEmbeddedDaemon || restartInProgress) return {};
return {operation, true, daemonRunning, "daemon-restart", "Restarting daemon...", "",
true, false, true, 500};
case LifecycleOperation::Rescan:
if (!usingEmbeddedDaemon || !hasDaemon) {
return {operation, false, daemonRunning, "", "",
"Rescan requires embedded daemon. Restart your daemon with -rescan manually."};
}
return {operation, true, daemonRunning, "rescan-blockchain", "Starting rescan...", "",
false, true, false, 3000};
case LifecycleOperation::RepairWallet:
if (!usingEmbeddedDaemon || !hasDaemon) {
return {operation, false, daemonRunning, "", "",
"Wallet repair requires embedded daemon. Restart your daemon with -zapwallettxes=2 manually."};
}
return {operation, true, daemonRunning, "repair-wallet", "Repairing wallet...", "",
false, false, false, 3000, true};
case LifecycleOperation::DeleteBlockchainData:
if (!usingEmbeddedDaemon || !hasDaemon) {
return {operation, false, daemonRunning, "", "",
"Delete blockchain requires embedded daemon. Stop your daemon manually and delete the data directory."};
}
return {operation, true, daemonRunning, "delete-blockchain-data", "Deleting blockchain data...", "",
false, false, false, 3000};
case LifecycleOperation::BootstrapStop:
return {operation, true, daemonRunning, "bootstrap-stop-daemon", "Stopping daemon for bootstrap...", "",
false, false, true, 0};
}
return {};
}
void prepareLifecycleOperation(const LifecycleDecision& decision,
const config::Settings* settings = nullptr);
static inline LifecycleExecutionResult executeLifecycleOperation(const LifecycleDecision& decision,
LifecycleRuntime& runtime,
LifecycleTaskContext& task)
{
LifecycleExecutionResult result;
if (!decision.allowed) return result;
auto waitForDelay = [&]() {
int waitTicks = std::max(0, decision.restartDelayMs / 100);
for (int i = 0; i < waitTicks && !task.cancelled() && !task.shuttingDown(); ++i) {
task.sleepForMs(100);
}
};
auto cancelled = [&]() {
result.cancelled = task.cancelled() || task.shuttingDown();
return result.cancelled;
};
switch (decision.operation) {
case LifecycleOperation::BootstrapStop:
if (decision.wasRunning) {
runtime.requestRpcStopAndDisconnect("Bootstrap daemon stop", "Bootstrap");
result.stopped = true;
}
result.completed = true;
return result;
case LifecycleOperation::ManualRestart:
if (decision.wasRunning) {
runtime.stopDaemonWithPolicy();
result.stopped = true;
}
break;
case LifecycleOperation::Rescan:
case LifecycleOperation::RepairWallet:
case LifecycleOperation::DeleteBlockchainData:
runtime.stopDaemonWithPolicy();
result.stopped = true;
break;
}
if (cancelled()) return result;
waitForDelay();
if (cancelled()) return result;
if (decision.operation == LifecycleOperation::DeleteBlockchainData) {
result.deletedItems = runtime.deleteBlockchainData();
if (cancelled()) return result;
}
if (decision.operation == LifecycleOperation::Rescan ||
decision.operation == LifecycleOperation::RepairWallet ||
decision.operation == LifecycleOperation::DeleteBlockchainData) {
runtime.resetOutputOffset();
}
result.started = runtime.startDaemon();
result.completed = !cancelled();
return result;
}
ShutdownDecision shutdownDecision(bool keepDaemonRunning,
bool stopExternalDaemon) const;
private:
std::unique_ptr<EmbeddedDaemon> daemon_;
};
} // namespace daemon
} // namespace dragonx