4 Commits

Author SHA1 Message Date
dan_s
b3d43ba0ad update build output filenames to include version info 2026-03-25 11:24:21 -05:00
430290f97a update hardcoded version for mac dmg build 2026-03-25 11:18:03 -05:00
dan_s
30fc5da520 feat: track shielded send txids via z_viewtransaction
Extract txids from completed z_sendmany operations and store in
send_txids_ so pure shielded sends are discoverable. The network
thread includes them in the enrichment set, calls z_viewtransaction,
caches results in viewtx_cache_, and removes them from send_txids_.
2026-03-25 11:06:09 -05:00
f02c965929 fix: macOS block index corruption, dbcache auto-sizing, import key rescan height
- Shutdown: 3-phase stop (wait for RPC stop → SIGTERM → SIGKILL) prevents
  LevelDB flush interruption on macOS/APFS that caused full re-sync on restart
- dbcache: auto-detect RAM and set -dbcache to 12.5% (clamped 450-4096 MB)
  on macOS (sysctl), Linux (sysconf), and Windows (GlobalMemoryStatusEx)
- Import key: pass user-entered start height to z_importkey and trigger
  rescanblockchain from that height for t-key imports
- Bump version to 1.1.1
2026-03-25 11:00:14 -05:00
11 changed files with 160 additions and 37 deletions

View File

@@ -4,7 +4,7 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(ObsidianDragon project(ObsidianDragon
VERSION 1.1.0 VERSION 1.1.1
LANGUAGES C CXX LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet" DESCRIPTION "DragonX Cryptocurrency Wallet"
) )

View File

@@ -20,7 +20,7 @@
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="1.0.0" VERSION="1.1.1"
# ── Colours ────────────────────────────────────────────────────────────────── # ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m' RED='\033[0;31m'
@@ -261,7 +261,7 @@ build_release_linux() {
rm -rf "$out" rm -rf "$out"
mkdir -p "$out" mkdir -p "$out"
local DIST="ObsidianDragon-Linux-x64" local DIST="ObsidianDragon-${VERSION}-Linux-x64"
local dist_dir="$out/$DIST" local dist_dir="$out/$DIST"
mkdir -p "$dist_dir" mkdir -p "$dist_dir"
@@ -379,9 +379,9 @@ APPRUN
local ARCH local ARCH
ARCH=$(uname -m) ARCH=$(uname -m)
cd "$bd" cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${ARCH}.AppImage" 2>/dev/null && { ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
cp "ObsidianDragon-${ARCH}.AppImage" "$out/ObsidianDragon.AppImage" cp "ObsidianDragon-${VERSION}-${ARCH}.AppImage" "$out/ObsidianDragon-${VERSION}.AppImage"
info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))" info "AppImage: $out/ObsidianDragon-${VERSION}.AppImage ($(du -h "$out/ObsidianDragon-${VERSION}.AppImage" | cut -f1))"
} || warn "AppImage creation failed — binaries zip still in release/linux/" } || warn "AppImage creation failed — binaries zip still in release/linux/"
info "Linux release artifacts: $out/" info "Linux release artifacts: $out/"
@@ -604,7 +604,7 @@ HDR
rm -rf "$out" rm -rf "$out"
mkdir -p "$out" mkdir -p "$out"
local DIST="ObsidianDragon-Windows-x64" local DIST="ObsidianDragon-${VERSION}-Windows-x64"
local dist_dir="$out/$DIST" local dist_dir="$out/$DIST"
mkdir -p "$dist_dir" mkdir -p "$dist_dir"
cp bin/ObsidianDragon.exe "$dist_dir/" cp bin/ObsidianDragon.exe "$dist_dir/"
@@ -628,8 +628,8 @@ HDR
cp -r bin/res "$dist_dir/" 2>/dev/null || true cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Single-file exe (all resources embedded) ──────────────────────────── # ── Single-file exe (all resources embedded) ────────────────────────────
cp bin/ObsidianDragon.exe "$out/" cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe"
info "Single-file exe: $out/ObsidianDragon.exe ($(du -h "$out/ObsidianDragon.exe" | cut -f1))" info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))"
# ── Zip ────────────────────────────────────────────────────────────────── # ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then if command -v zip &>/dev/null; then
@@ -1019,6 +1019,13 @@ PLIST
info ".app bundle created: $APP" info ".app bundle created: $APP"
# ── Zip the .app bundle ──────────────────────────────────────────────────
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
fi
# ── Create DMG ─────────────────────────────────────────────────────────── # ── Create DMG ───────────────────────────────────────────────────────────
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg" local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"

View File

@@ -5,7 +5,7 @@
<assemblyIdentity <assemblyIdentity
type="win32" type="win32"
name="DragonX.ObsidianDragon.Wallet" name="DragonX.ObsidianDragon.Wallet"
version="1.1.0.0" version="1.1.1.0"
processorArchitecture="amd64" processorArchitecture="amd64"
/> />

View File

@@ -8,7 +8,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux" BUILD_DIR="${SCRIPT_DIR}/build/linux"
APPDIR="${BUILD_DIR}/AppDir" APPDIR="${BUILD_DIR}/AppDir"
VERSION="1.0.0" VERSION="1.1.1"
# Colors # Colors
GREEN='\033[0;32m' GREEN='\033[0;32m'

View File

@@ -7,7 +7,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux" BUILD_DIR="${SCRIPT_DIR}/build/linux"
VERSION="1.0.0" VERSION="1.1.1"
# Colors for output # Colors for output
RED='\033[0;31m' RED='\033[0;31m'

View File

@@ -564,13 +564,26 @@ void App::update()
}; };
} }
} }
return [this, done, anySuccess]() { // Extract txids from successful operations so shielded
// sends are discoverable by z_viewtransaction.
std::vector<std::string> successTxids;
for (const auto& op : result) {
if (op.value("status", "") == "success"
&& op.contains("result") && op["result"].contains("txid")) {
successTxids.push_back(op["result"]["txid"].get<std::string>());
}
}
return [this, done, anySuccess,
successTxids = std::move(successTxids)]() {
for (const auto& id : done) { for (const auto& id : done) {
pending_opids_.erase( pending_opids_.erase(
std::remove(pending_opids_.begin(), pending_opids_.end(), id), std::remove(pending_opids_.begin(), pending_opids_.end(), id),
pending_opids_.end()); pending_opids_.end());
} }
if (anySuccess) { if (anySuccess) {
for (const auto& txid : successTxids) {
send_txids_.insert(txid);
}
// Transaction confirmed by daemon — force immediate data refresh // Transaction confirmed by daemon — force immediate data refresh
transactions_dirty_ = true; transactions_dirty_ = true;
addresses_dirty_ = true; addresses_dirty_ = true;
@@ -2124,15 +2137,19 @@ void App::stopEmbeddedDaemon()
} }
if (stop_sent) { if (stop_sent) {
DEBUG_LOGF("Waiting for daemon to begin shutdown...\n"); DEBUG_LOGF("Waiting for daemon to flush block index and shut down...\n");
shutdown_status_ = "Waiting for daemon to begin shutdown..."; shutdown_status_ = "Waiting for daemon to flush block index...";
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 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..."; 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 bool App::isEmbeddedDaemonRunning() const

View File

@@ -494,6 +494,12 @@ private:
float opid_poll_timer_ = 0.0f; float opid_poll_timer_ = 0.0f;
static constexpr float OPID_POLL_INTERVAL = 2.0f; static constexpr float OPID_POLL_INTERVAL = 2.0f;
// Txids from completed z_sendmany operations.
// Ensures shielded sends are discoverable by z_viewtransaction
// even when they don't appear in listtransactions or
// z_listreceivedbyaddress.
std::unordered_set<std::string> send_txids_;
// First-run wizard state // First-run wizard state
WizardPhase wizard_phase_ = WizardPhase::None; WizardPhase wizard_phase_ = WizardPhase::None;
std::unique_ptr<util::Bootstrap> bootstrap_; std::unique_ptr<util::Bootstrap> bootstrap_;

View File

@@ -437,6 +437,9 @@ void App::refreshData()
// Snapshot viewtx cache for the worker thread // Snapshot viewtx cache for the worker thread
auto viewtxCacheSnap = viewtx_cache_; auto viewtxCacheSnap = viewtx_cache_;
// Snapshot send txids so the worker can include them in enrichment
auto sendTxidsSnap = send_txids_;
// Single consolidated worker task — all RPC calls happen back-to-back // Single consolidated worker task — all RPC calls happen back-to-back
// on a single thread with no inter-task queue overhead. // on a single thread with no inter-task queue overhead.
worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions, worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions,
@@ -444,7 +447,8 @@ void App::refreshData()
fullyEnriched = std::move(fullyEnriched), fullyEnriched = std::move(fullyEnriched),
cachedConfirmedTxns = std::move(cachedConfirmedTxns), cachedConfirmedTxns = std::move(cachedConfirmedTxns),
cachedConfirmedIds = std::move(cachedConfirmedIds), cachedConfirmedIds = std::move(cachedConfirmedIds),
viewtxCacheSnap = std::move(viewtxCacheSnap)]() -> rpc::RPCWorker::MainCb { viewtxCacheSnap = std::move(viewtxCacheSnap),
sendTxidsSnap = std::move(sendTxidsSnap)]() -> rpc::RPCWorker::MainCb {
// ================================================================ // ================================================================
// Phase 1: Balance + blockchain info // Phase 1: Balance + blockchain info
// ================================================================ // ================================================================
@@ -596,6 +600,13 @@ void App::refreshData()
} }
} }
// Include txids from completed z_sendmany operations so that
// pure shielded sends (which don't appear in listtransactions
// or z_listreceivedbyaddress) are discoverable.
for (const auto& txid : sendTxidsSnap) {
knownTxids.insert(txid);
}
// Phase 3c: detect shielded sends via z_viewtransaction // Phase 3c: detect shielded sends via z_viewtransaction
// Check the in-memory viewtx cache first; only make RPC calls // Check the in-memory viewtx cache first; only make RPC calls
// for txids we haven't seen before. // for txids we haven't seen before.
@@ -858,6 +869,8 @@ void App::refreshData()
// Merge new z_viewtransaction results into the persistent cache // Merge new z_viewtransaction results into the persistent cache
for (auto& [txid, entry] : newViewTxEntries) { for (auto& [txid, entry] : newViewTxEntries) {
viewtx_cache_[txid] = std::move(entry); viewtx_cache_[txid] = std::move(entry);
// Once cached, no need to keep in send_txids_
send_txids_.erase(txid);
} }
// Rebuild confirmed transaction cache: txns with >= 10 // Rebuild confirmed transaction cache: txns with >= 10

View File

@@ -7,10 +7,10 @@
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake. // !! 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 ...) // !! 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_MAJOR 1
#define DRAGONX_VERSION_MINOR 1 #define DRAGONX_VERSION_MINOR 1
#define DRAGONX_VERSION_PATCH 0 #define DRAGONX_VERSION_PATCH 1
#define DRAGONX_APP_NAME "ObsidianDragon" #define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush" #define DRAGONX_ORG_NAME "Hush"

View File

@@ -34,6 +34,9 @@
#include <sys/socket.h> #include <sys/socket.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#ifdef __APPLE__
#include <sys/sysctl.h>
#endif
#endif #endif
namespace fs = std::filesystem; namespace fs = std::filesystem;
@@ -152,6 +155,41 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
// DragonX chain parameters. // DragonX chain parameters.
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout. // On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
// On Linux, -printtoconsole is used for pipe-based output capture. // 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<int>(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<int>((static_cast<int64_t>(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<int>(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 { return {
"-tls=only", "-tls=only",
#ifndef _WIN32 #ifndef _WIN32
@@ -166,7 +204,8 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
"-ac_private=1", "-ac_private=1",
"-addnode=176.126.87.241", "-addnode=176.126.87.241",
"-experimentalfeatures", "-experimentalfeatures",
"-developerencryptwallet" "-developerencryptwallet",
dbcache_arg
}; };
} }
@@ -1045,14 +1084,12 @@ void EmbeddedDaemon::stop(int wait_ms)
if (process_pid_ > 0) { if (process_pid_ > 0) {
setState(State::Stopping, "Stopping dragonxd..."); setState(State::Stopping, "Stopping dragonxd...");
// Send SIGTERM to the entire process group (negative PID). // Phase 1: Wait for the daemon to exit naturally.
// This ensures that if dragonxd is a shell script wrapper, // The caller (stopEmbeddedDaemon) already sent an RPC "stop" which
// both bash AND the actual dragonxd child receive the signal. // tells the daemon to flush LevelDB, close sockets, and exit cleanly.
// Without this, only bash is killed and dragonxd is orphaned. // On macOS/APFS the LevelDB flush can take several seconds — we must
DEBUG_LOGF("Sending SIGTERM to process group -%d\n", process_pid_); // NOT send SIGTERM until the daemon has had enough time to finish.
kill(-process_pid_, SIGTERM); DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms);
// Wait for process to exit, draining stdout each iteration
auto start = std::chrono::steady_clock::now(); auto start = std::chrono::steady_clock::now();
while (isRunning()) { while (isRunning()) {
drainOutput(); drainOutput();
@@ -1061,15 +1098,34 @@ void EmbeddedDaemon::stop(int wait_ms)
std::chrono::steady_clock::now() - start).count(); std::chrono::steady_clock::now() - start).count();
if (elapsed >= wait_ms) { 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; break;
} }
std::this_thread::sleep_for(std::chrono::milliseconds(100)); 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::milliseconds>(
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 drainOutput(); // read any remaining output
// Reap the child process // Reap the child process

View File

@@ -307,8 +307,9 @@ void ImportKeyDialog::render(App* app)
// Import keys on worker thread to avoid freezing UI // Import keys on worker thread to avoid freezing UI
bool rescan = s_rescan; bool rescan = s_rescan;
int rescanHeight = s_rescan_height;
if (app->worker()) { 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 imported = 0;
int failed = 0; int failed = 0;
@@ -317,10 +318,22 @@ void ImportKeyDialog::render(App* app)
try { try {
if (keyType == "z-spending") { 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++; imported++;
} else if (keyType == "t-privkey") { } 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++; imported++;
} else { } else {
failed++; failed++;
@@ -330,6 +343,17 @@ void ImportKeyDialog::render(App* app)
} }
} }
// 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]() { return [imported, failed]() {
s_imported_keys = imported; s_imported_keys = imported;
s_failed_keys = failed; s_failed_keys = failed;