diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7abb8f7..9f82863 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,7 +4,7 @@
cmake_minimum_required(VERSION 3.20)
project(ObsidianDragon
- VERSION 1.1.0
+ VERSION 1.1.1
LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet"
)
diff --git a/res/ObsidianDragon.manifest b/res/ObsidianDragon.manifest
index 7856e20..2952366 100644
--- a/res/ObsidianDragon.manifest
+++ b/res/ObsidianDragon.manifest
@@ -5,7 +5,7 @@
diff --git a/src/app.cpp b/src/app.cpp
index 6825c90..eb9e398 100644
--- a/src/app.cpp
+++ b/src/app.cpp
@@ -2124,15 +2124,19 @@ void App::stopEmbeddedDaemon()
}
if (stop_sent) {
- DEBUG_LOGF("Waiting for daemon to begin shutdown...\n");
- shutdown_status_ = "Waiting for daemon to begin shutdown...";
- std::this_thread::sleep_for(std::chrono::milliseconds(500));
+ DEBUG_LOGF("Waiting for daemon to flush block index and shut down...\n");
+ shutdown_status_ = "Waiting for daemon to flush block index...";
+ // Give the daemon time to flush LevelDB to disk before we
+ // escalate to SIGTERM. On macOS/APFS, LevelDB compaction +
+ // fsync can take 15-20s on a large chain. The stop() method
+ // will wait this long for a *natural* exit (via the RPC stop
+ // we already sent) before falling back to SIGTERM.
}
- // Wait for process to exit; SIGTERM/TerminateProcess as last resort.
- // 10 seconds is generous — if the daemon hasn't exited by then it's stuck.
shutdown_status_ = "Waiting for dragonxd process to exit...";
- embedded_daemon_->stop(10000);
+ // 20s grace period for the RPC "stop" to complete (LevelDB flush).
+ // Only after that does stop() escalate to SIGTERM, then SIGKILL.
+ embedded_daemon_->stop(20000);
}
bool App::isEmbeddedDaemonRunning() const
diff --git a/src/config/version.h b/src/config/version.h
index 9aece2b..05485b5 100644
--- a/src/config/version.h
+++ b/src/config/version.h
@@ -7,10 +7,10 @@
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
-#define DRAGONX_VERSION "1.1.0"
+#define DRAGONX_VERSION "1.1.1"
#define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 1
-#define DRAGONX_VERSION_PATCH 0
+#define DRAGONX_VERSION_PATCH 1
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush"
diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp
index 97f0129..3f2642a 100644
--- a/src/daemon/embedded_daemon.cpp
+++ b/src/daemon/embedded_daemon.cpp
@@ -34,6 +34,9 @@
#include
#include
#include
+#ifdef __APPLE__
+#include
+#endif
#endif
namespace fs = std::filesystem;
@@ -152,6 +155,41 @@ std::vector EmbeddedDaemon::getChainParams()
// DragonX chain parameters.
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
// On Linux, -printtoconsole is used for pipe-based output capture.
+ // Auto-detect a reasonable -dbcache based on available physical RAM.
+ // Default LevelDB cache is small (~450MB); larger caches improve sync
+ // performance and reduce disk I/O — especially on macOS with APFS.
+ std::string dbcache_arg = "-dbcache=450";
+ {
+#ifdef __APPLE__
+ // sysctl hw.memsize returns total physical RAM in bytes
+ int64_t memsize = 0;
+ size_t len = sizeof(memsize);
+ if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) {
+ int totalMB = static_cast(memsize / (1024 * 1024));
+ // Use ~12.5% of RAM for dbcache, clamped to [450, 4096]
+ int cache = std::max(450, std::min(4096, totalMB / 8));
+ dbcache_arg = "-dbcache=" + std::to_string(cache);
+ }
+#elif defined(__linux__)
+ long pages = sysconf(_SC_PHYS_PAGES);
+ long page_size = sysconf(_SC_PAGE_SIZE);
+ if (pages > 0 && page_size > 0) {
+ int totalMB = static_cast((static_cast(pages) * page_size) / (1024 * 1024));
+ int cache = std::max(450, std::min(4096, totalMB / 8));
+ dbcache_arg = "-dbcache=" + std::to_string(cache);
+ }
+#elif defined(_WIN32)
+ MEMORYSTATUSEX memInfo;
+ memInfo.dwLength = sizeof(memInfo);
+ if (GlobalMemoryStatusEx(&memInfo)) {
+ int totalMB = static_cast(memInfo.ullTotalPhys / (1024 * 1024));
+ int cache = std::max(450, std::min(4096, totalMB / 8));
+ dbcache_arg = "-dbcache=" + std::to_string(cache);
+ }
+#endif
+ DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
+ }
+
return {
"-tls=only",
#ifndef _WIN32
@@ -166,7 +204,8 @@ std::vector EmbeddedDaemon::getChainParams()
"-ac_private=1",
"-addnode=176.126.87.241",
"-experimentalfeatures",
- "-developerencryptwallet"
+ "-developerencryptwallet",
+ dbcache_arg
};
}
@@ -1045,14 +1084,12 @@ void EmbeddedDaemon::stop(int wait_ms)
if (process_pid_ > 0) {
setState(State::Stopping, "Stopping dragonxd...");
- // Send SIGTERM to the entire process group (negative PID).
- // This ensures that if dragonxd is a shell script wrapper,
- // both bash AND the actual dragonxd child receive the signal.
- // Without this, only bash is killed and dragonxd is orphaned.
- DEBUG_LOGF("Sending SIGTERM to process group -%d\n", process_pid_);
- kill(-process_pid_, SIGTERM);
-
- // Wait for process to exit, draining stdout each iteration
+ // Phase 1: Wait for the daemon to exit naturally.
+ // The caller (stopEmbeddedDaemon) already sent an RPC "stop" which
+ // tells the daemon to flush LevelDB, close sockets, and exit cleanly.
+ // On macOS/APFS the LevelDB flush can take several seconds — we must
+ // NOT send SIGTERM until the daemon has had enough time to finish.
+ DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms);
auto start = std::chrono::steady_clock::now();
while (isRunning()) {
drainOutput();
@@ -1061,15 +1098,34 @@ void EmbeddedDaemon::stop(int wait_ms)
std::chrono::steady_clock::now() - start).count();
if (elapsed >= wait_ms) {
- // Force kill the entire process group
- DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
- kill(-process_pid_, SIGKILL);
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
+ // Phase 2: If still running, send SIGTERM and wait a further 10s.
+ if (isRunning()) {
+ DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", process_pid_);
+ kill(-process_pid_, SIGTERM);
+
+ auto sigterm_start = std::chrono::steady_clock::now();
+ while (isRunning()) {
+ drainOutput();
+ auto elapsed = std::chrono::duration_cast(
+ std::chrono::steady_clock::now() - sigterm_start).count();
+ if (elapsed >= 10000) {
+ // Phase 3: Force kill
+ DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
+ kill(-process_pid_, SIGKILL);
+ break;
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ }
+ } else {
+ DEBUG_LOGF("Daemon exited cleanly after RPC stop\n");
+ }
+
drainOutput(); // read any remaining output
// Reap the child process
diff --git a/src/ui/windows/import_key_dialog.cpp b/src/ui/windows/import_key_dialog.cpp
index c317ed4..8074d04 100644
--- a/src/ui/windows/import_key_dialog.cpp
+++ b/src/ui/windows/import_key_dialog.cpp
@@ -307,8 +307,9 @@ void ImportKeyDialog::render(App* app)
// Import keys on worker thread to avoid freezing UI
bool rescan = s_rescan;
+ int rescanHeight = s_rescan_height;
if (app->worker()) {
- app->worker()->post([rpc = app->rpc(), keys, rescan]() -> rpc::RPCWorker::MainCb {
+ app->worker()->post([rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb {
int imported = 0;
int failed = 0;
@@ -317,10 +318,22 @@ void ImportKeyDialog::render(App* app)
try {
if (keyType == "z-spending") {
- rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
+ // z_importkey "key" "yes"|"no" startheight
+ if (rescan && rescanHeight > 0) {
+ rpc->call("z_importkey", {key, "yes", rescanHeight});
+ } else {
+ rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
+ }
imported++;
} else if (keyType == "t-privkey") {
- rpc->call("importprivkey", {key, "", rescan});
+ // importprivkey does not accept a startheight;
+ // import without rescan, then rescanblockchain
+ // from the requested height afterward.
+ if (rescan && rescanHeight > 0) {
+ rpc->call("importprivkey", {key, "", false});
+ } else {
+ rpc->call("importprivkey", {key, "", rescan});
+ }
imported++;
} else {
failed++;
@@ -329,6 +342,17 @@ void ImportKeyDialog::render(App* app)
failed++;
}
}
+
+ // If t-keys were imported without rescan (because a
+ // specific start height was requested), trigger a
+ // single rescanblockchain from that height now.
+ if (rescan && rescanHeight > 0 && imported > 0) {
+ try {
+ rpc->call("rescanblockchain", {rescanHeight});
+ } catch (...) {
+ // rescan failure is non-fatal; user can retry
+ }
+ }
return [imported, failed]() {
s_imported_keys = imported;