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>
239 lines
9.1 KiB
C++
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
|