Compare commits
10 Commits
fix/audit-
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 45b652f514 | |||
| fffee9f0b5 | |||
| 00ffc959e5 | |||
| b561406c23 | |||
| 5a0743a17b | |||
| a7b0770ad0 | |||
| 7d8323a622 | |||
| 320944fd18 | |||
| 6d5e0ac614 | |||
| 21da9e75fc |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,8 +11,8 @@ prebuilt-binaries/dragonxd-win/*
|
||||
!prebuilt-binaries/dragonxd-win/.gitkeep
|
||||
prebuilt-binaries/dragonxd-mac/*
|
||||
!prebuilt-binaries/dragonxd-mac/.gitkeep
|
||||
prebuilt-binaries/xmrig-hac/*
|
||||
!prebuilt-binaries/xmrig-hac/.gitkeep
|
||||
prebuilt-binaries/drg-xmrig/*
|
||||
!prebuilt-binaries/drg-xmrig/.gitkeep
|
||||
|
||||
|
||||
# External sources / toolchains (created by scripts/setup.sh)
|
||||
@@ -33,7 +33,7 @@ imgui.ini
|
||||
*.bak*
|
||||
*.params
|
||||
asmap.dat
|
||||
/external/xmrig-hac
|
||||
/external/drg-xmrig
|
||||
/memory
|
||||
/todo.md
|
||||
/.github/
|
||||
|
||||
@@ -136,6 +136,13 @@ if(DRAGONX_ENABLE_LITE_BACKEND)
|
||||
set_target_properties(dragonx_lite_backend PROPERTIES
|
||||
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
|
||||
)
|
||||
if(APPLE)
|
||||
# The Rust backend's TLS stack (security-framework / core-foundation crates)
|
||||
# references Secure Transport (SSL*) + CoreFoundation symbols. Link the frameworks
|
||||
# that provide them, or the static lib leaves ~130 symbols undefined at link time.
|
||||
set_property(TARGET dragonx_lite_backend APPEND PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES "-framework Security" "-framework CoreFoundation")
|
||||
endif()
|
||||
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
|
||||
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||
@@ -284,6 +291,15 @@ FetchContent_Declare(
|
||||
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
|
||||
GIT_TAG v1.4.0
|
||||
GIT_SHALLOW TRUE
|
||||
# libwebp's cpu.cmake applies -mno-sse2/-mno-sse4.1 to its scalar reference DSP
|
||||
# files when it can't probe SSE support. Under a macOS universal build
|
||||
# (-arch arm64;x86_64) that probe fails, so the flags land on the x86_64 slice,
|
||||
# where -mno-sse2 disables _Float16 and breaks the SDK's <math.h>. Neutralize
|
||||
# those disable flags (SSE2 is x86_64 baseline). Portable + idempotent; a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. See cmake/patch-libwebp-simd.cmake.
|
||||
PATCH_COMMAND ${CMAKE_COMMAND}
|
||||
-DCPU_CMAKE=<SOURCE_DIR>/cmake/cpu.cmake
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/patch-libwebp-simd.cmake
|
||||
)
|
||||
set(WEBP_LINK_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
|
||||
|
||||
@@ -81,8 +81,8 @@ Download linux and windows binaries of latest releases and place in binary direc
|
||||
- prebuilt-binaries/dragonxd-win/
|
||||
- prebuilt-binaries/dragonxd-mac/
|
||||
|
||||
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac):
|
||||
- prebuilt-binaries/xmrig-hac/
|
||||
**DRG-XMRig fork** (https://git.dragonx.is/DragonX/drg-xmrig):
|
||||
- prebuilt-binaries/drg-xmrig/
|
||||
|
||||
|
||||
## Build Steps
|
||||
|
||||
54
build.sh
54
build.sh
@@ -131,7 +131,7 @@ fi
|
||||
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
|
||||
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
|
||||
_cml="$SCRIPT_DIR/CMakeLists.txt"
|
||||
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
|
||||
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]][[:space:]]*\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
|
||||
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||
@@ -386,7 +386,7 @@ build_release_linux() {
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
|
||||
@@ -419,7 +419,7 @@ build_release_linux() {
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
|
||||
|
||||
# Desktop entry
|
||||
@@ -638,8 +638,8 @@ HDR
|
||||
info "Lite mode: skipping embedded daemon binaries"
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
|
||||
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
|
||||
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
|
||||
@@ -791,7 +791,7 @@ HDR
|
||||
fi
|
||||
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig.exe"
|
||||
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
|
||||
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
@@ -901,8 +901,26 @@ build_release_mac() {
|
||||
fi
|
||||
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
||||
else
|
||||
# Native macOS: build universal binary (arm64 + x86_64)
|
||||
# Native macOS: build universal (arm64 + x86_64) by default. Override with
|
||||
# DRAGONX_MAC_ARCHS (e.g. "x86_64").
|
||||
MAC_ARCHS="${DRAGONX_MAC_ARCHS:-arm64;x86_64}"
|
||||
# When linking the real lite backend, the app can only include architectures
|
||||
# the backend static library actually provides. Its pinned ring 0.16.11 has no
|
||||
# Apple-Silicon assembly, so that artifact is x86_64-only — constrain the app
|
||||
# arch to the backend's (unless the user explicitly forced DRAGONX_MAC_ARCHS),
|
||||
# otherwise the arm64 slice fails to link.
|
||||
if $DO_LITE_BACKEND && [[ -z "${DRAGONX_MAC_ARCHS:-}" && -n "${lb_lib:-}" ]] && command -v lipo &>/dev/null; then
|
||||
local _backend_archs; _backend_archs=$(lipo -archs "$lb_lib" 2>/dev/null | tr ' ' ';')
|
||||
if [[ -n "$_backend_archs" && "$_backend_archs" != "$MAC_ARCHS" ]]; then
|
||||
warn "Lite backend provides only [$_backend_archs] — building the app for that instead of universal."
|
||||
MAC_ARCHS="$_backend_archs"
|
||||
fi
|
||||
fi
|
||||
if [[ "$MAC_ARCHS" == *";"* || "$MAC_ARCHS" == *","* ]]; then
|
||||
MAC_ARCH="universal"
|
||||
else
|
||||
MAC_ARCH="$MAC_ARCHS"
|
||||
fi
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
fi
|
||||
|
||||
@@ -990,7 +1008,7 @@ TOOLCHAIN
|
||||
need_sodium=true
|
||||
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||
# Rebuild if existing lib is not universal (single-arch won't link)
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -Eq "arm64.*x86_64|x86_64.*arm64"; then
|
||||
info "Existing libsodium is not universal — rebuilding ..."
|
||||
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||
need_sodium=true
|
||||
@@ -1001,13 +1019,13 @@ TOOLCHAIN
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||
fi
|
||||
|
||||
info "Configuring (native universal arm64+x86_64) ..."
|
||||
info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
|
||||
-DCMAKE_OSX_ARCHITECTURES="$MAC_ARCHS" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
fi
|
||||
|
||||
@@ -1037,8 +1055,12 @@ TOOLCHAIN
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||
|
||||
# ── Create .app bundle ───────────────────────────────────────────────────
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
# Clean only THIS variant's prior artifacts so full-node and lite releases can
|
||||
# coexist in release/mac/ (Linux/Windows scope their cleanup the same way). The
|
||||
# "ObsidianDragon-" glob never matches "ObsidianDragonLite-" (and vice versa),
|
||||
# and the ".app" names are exact.
|
||||
rm -rf "$out/${APP_BASENAME}.app" "$out/${APP_BASENAME}-"*.app.zip "$out/${APP_BASENAME}-"*.dmg
|
||||
|
||||
local APP="$out/${APP_BASENAME}.app"
|
||||
local CONTENTS="$APP/Contents"
|
||||
@@ -1088,8 +1110,8 @@ TOOLCHAIN
|
||||
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||
fi
|
||||
|
||||
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
# xmrig binary (from prebuilt-binaries/drg-xmrig/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
if [[ -f "$XMRIG_MAC" ]]; then
|
||||
cp "$XMRIG_MAC" "$MACOS/xmrig"
|
||||
chmod +x "$MACOS/xmrig"
|
||||
@@ -1238,8 +1260,10 @@ PLIST
|
||||
fi
|
||||
|
||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||
local DMG_BASENAME="DragonX_Wallet"
|
||||
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
|
||||
# DMG filename matches the app bundle name (ObsidianDragon / ObsidianDragonLite).
|
||||
# The mounted volume + CFBundleName keep the "DragonX Wallet" display branding
|
||||
# (APP_DISPLAY_NAME above).
|
||||
local DMG_BASENAME="${APP_BASENAME}"
|
||||
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||
|
||||
if command -v create-dmg &>/dev/null; then
|
||||
|
||||
31
cmake/patch-libwebp-simd.cmake
Normal file
31
cmake/patch-libwebp-simd.cmake
Normal file
@@ -0,0 +1,31 @@
|
||||
# patch-libwebp-simd.cmake — portable, idempotent FetchContent patch for libwebp.
|
||||
#
|
||||
# libwebp's cmake/cpu.cmake compiles its scalar *reference* DSP files with the
|
||||
# SSE-disable flags "-mno-sse4.1;-mno-sse2" whenever it can't positively detect
|
||||
# SSE support. Under a macOS *universal* build (-arch arm64;x86_64) the per-arch
|
||||
# SSE flag probe fails (a flag valid for x86_64 is invalid for arm64), so those
|
||||
# disable flags get applied to the x86_64 slice. clang gates the _Float16 type on
|
||||
# SSE2 for x86_64, and the macOS 15+/26 SDK's <math.h> declares _Float16 math
|
||||
# functions unconditionally — so any TU including <math.h> fails to compile with
|
||||
# "_Float16 is not supported on this target".
|
||||
#
|
||||
# SSE2 is part of the x86_64 baseline ABI, so disabling it on the reference files
|
||||
# is unnecessary on every platform we target. Blanking the SSE entries (indices
|
||||
# must stay aligned with WEBP_SIMD_FLAGS) fixes the universal build and is a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. Idempotent: re-running is a no-op.
|
||||
if(NOT DEFINED CPU_CMAKE OR NOT EXISTS "${CPU_CMAKE}")
|
||||
message(FATAL_ERROR "patch-libwebp-simd: cpu.cmake not found at '${CPU_CMAKE}'")
|
||||
endif()
|
||||
|
||||
file(READ "${CPU_CMAKE}" _contents)
|
||||
string(REPLACE
|
||||
"set(SIMD_DISABLE_FLAGS \"-mno-sse4.1;-mno-sse2;;-mno-dspr2;;-mno-msa\")"
|
||||
"set(SIMD_DISABLE_FLAGS \";;;-mno-dspr2;;-mno-msa\")"
|
||||
_patched "${_contents}")
|
||||
|
||||
if(_patched STREQUAL _contents)
|
||||
message(STATUS "patch-libwebp-simd: no change (already patched or pattern absent)")
|
||||
else()
|
||||
file(WRITE "${CPU_CMAKE}" "${_patched}")
|
||||
message(STATUS "patch-libwebp-simd: neutralized x86 SSE-disable flags in cpu.cmake")
|
||||
endif()
|
||||
0
prebuilt-binaries/drg-xmrig/.gitkeep
Normal file
0
prebuilt-binaries/drg-xmrig/.gitkeep
Normal file
@@ -1,5 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script uses bash 4+ features (mapfile, safe empty-array expansion under
|
||||
# `set -u`). macOS ships bash 3.2, so re-exec under a newer bash when one is
|
||||
# present (Homebrew), and fail with a clear message otherwise.
|
||||
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
|
||||
for _newer_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
|
||||
[ -x "$_newer_bash" ] && exec "$_newer_bash" "$0" "$@"
|
||||
done
|
||||
echo "ERROR: build-lite-backend-artifact.sh requires bash 4+ (found ${BASH_VERSION:-unknown})." >&2
|
||||
echo " On macOS: brew install bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -312,6 +324,9 @@ prepare_backend_source() {
|
||||
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
|
||||
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
|
||||
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
||||
# Honor the pinned Rust toolchain (rust-toolchain.toml) inside the prepared root too,
|
||||
# so builds using --silentdragonxlitelib-dir still select rustc 1.63.
|
||||
[[ -f "$BACKEND_SOURCE_DIR/rust-toolchain.toml" ]] && ln -s "$BACKEND_SOURCE_DIR/rust-toolchain.toml" "$prepared_root/rust-toolchain.toml"
|
||||
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
|
||||
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
|
||||
# "vendor" relative to the build root, so expose it inside the prepared root too.
|
||||
|
||||
@@ -256,8 +256,8 @@ HEADER_START
|
||||
echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}"
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
|
||||
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
|
||||
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sign dragonx full-node release archives for the wallet's in-app daemon updater (ed25519).
|
||||
# Package the prebuilt dragonx full-node binaries into per-platform release archives and sign them
|
||||
# for the wallet's in-app daemon updater (ed25519 over the EXACT archive bytes).
|
||||
#
|
||||
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
|
||||
# key pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is
|
||||
# MANDATORY (kDaemonRequireSignature = true): an in-app update is refused unless a valid signature
|
||||
# is published. For each archive <name>.zip this produces <name>.zip.sig holding the base64 of the
|
||||
# raw 64-byte ed25519 signature — upload that .sig next to the .zip as a release asset.
|
||||
# The wallet verifies a detached ed25519 signature over the archive bytes against a public key
|
||||
# pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is MANDATORY
|
||||
# (kDaemonRequireSignature = true): an in-app update is refused unless a valid "<archive>.sig" is
|
||||
# published next to the archive. The wallet also checks each archive's SHA-256 against a markdown
|
||||
# checksum table in the release body, so `release` prints that table for you to paste in.
|
||||
#
|
||||
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032),
|
||||
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
|
||||
# (the same flow the wallet's unit tests verify for the miner updater).
|
||||
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the
|
||||
# same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>...# -> <file>.sig per file
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>... # sign existing files -> <file>.sig
|
||||
# scripts/sign-daemon-release.sh release <secret.key> <version> [--src DIR] [--out DIR]
|
||||
# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,macos,
|
||||
# # win64}.zip, sign each, and print the SHA-256 checksum table. Platforms with no dragonxd
|
||||
# # binary staged are skipped.
|
||||
#
|
||||
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
|
||||
# Keep the secret key (.ed25519.key) OFFLINE (mode 600). Paste the base64 public key into
|
||||
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
||||
|
||||
@@ -27,6 +33,26 @@ command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25
|
||||
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
|
||||
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
|
||||
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}';
|
||||
else shasum -a 256 "$1" | awk '{print $1}'; fi
|
||||
}
|
||||
|
||||
# Detached ed25519 signature over the raw file bytes -> <file>.sig (base64 of the 64-byte sig).
|
||||
sign_file() {
|
||||
local key="$1" f="$2" raw
|
||||
raw="$(mktemp)"
|
||||
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||
openssl base64 -A -in "$raw" > "$f.sig"
|
||||
printf '\n' >> "$f.sig"
|
||||
rm -f "$raw"
|
||||
}
|
||||
|
||||
# platform -> (staging dir under prebuilt-binaries, release token, expected daemon binary name)
|
||||
plat_dir() { case "$1" in linux) echo dragonxd-linux;; mac) echo dragonxd-mac;; win) echo dragonxd-win;; esac; }
|
||||
plat_token() { case "$1" in linux) echo linux-amd64;; mac) echo macos;; win) echo win64;; esac; }
|
||||
plat_daemon() { case "$1" in win) echo dragonxd.exe;; *) echo dragonxd;; esac; }
|
||||
|
||||
cmd="${1:-}"; shift || true
|
||||
case "$cmd" in
|
||||
keygen)
|
||||
@@ -42,26 +68,90 @@ case "$cmd" in
|
||||
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
|
||||
echo " $pub"
|
||||
;;
|
||||
|
||||
pubkey)
|
||||
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
|
||||
pubkey_b64 "$1"
|
||||
;;
|
||||
|
||||
sign)
|
||||
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
||||
key="$1"; shift
|
||||
[ -f "$key" ] || die "no such key: $key"
|
||||
for f in "$@"; do
|
||||
[ -f "$f" ] || die "no such file: $f"
|
||||
raw="$(mktemp)"
|
||||
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||
openssl base64 -A -in "$raw" > "$f.sig"
|
||||
printf '\n' >> "$f.sig"
|
||||
rm -f "$raw"
|
||||
sign_file "$key" "$f"
|
||||
echo "signed: $f -> $f.sig"
|
||||
done
|
||||
echo "Upload each .sig as a release asset next to its archive."
|
||||
;;
|
||||
|
||||
release)
|
||||
[ $# -ge 2 ] || die "usage: release <secret.key> <version> [--src DIR] [--out DIR]"
|
||||
key="$1"; version="$2"; shift 2
|
||||
src="$PROJECT_ROOT/prebuilt-binaries"
|
||||
out="$PROJECT_ROOT/release/daemon"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--src) [ $# -ge 2 ] || die "--src needs a value"; src="$2"; shift 2 ;;
|
||||
--out) [ $# -ge 2 ] || die "--out needs a value"; out="$2"; shift 2 ;;
|
||||
*) die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[ -f "$key" ] || die "no such key: $key"
|
||||
[ -d "$src" ] || die "no such source dir: $src"
|
||||
command -v zip >/dev/null 2>&1 || die "zip not found (install 'zip')"
|
||||
mkdir -p "$out"
|
||||
|
||||
# Sanity: warn if this key does not match the public key pinned in the wallet (the wallet would
|
||||
# then reject every signature made with it — only expected when deliberately rotating the key).
|
||||
pinned="$(grep -oE '"[A-Za-z0-9+/]{43}="' "$PROJECT_ROOT/src/util/daemon_updater.h" 2>/dev/null | head -1 | tr -d '"')"
|
||||
mine="$(pubkey_b64 "$key")"
|
||||
if [ -n "$pinned" ] && [ "$pinned" != "$mine" ]; then
|
||||
echo "WARNING: this key's public key does not match the one pinned in daemon_updater.h:" >&2
|
||||
echo " signing key -> $mine" >&2
|
||||
echo " pinned key -> $pinned" >&2
|
||||
echo " The wallet will REJECT these signatures unless you are rotating the pinned key." >&2
|
||||
echo >&2
|
||||
fi
|
||||
|
||||
made=0
|
||||
table=""
|
||||
for plat in linux mac win; do
|
||||
d="$src/$(plat_dir "$plat")"
|
||||
daemon="$d/$(plat_daemon "$plat")"
|
||||
if [ ! -f "$daemon" ]; then
|
||||
echo "skip $plat: no $(plat_daemon "$plat") staged in $d" >&2
|
||||
continue
|
||||
fi
|
||||
archive="dragonx-$version-$(plat_token "$plat").zip"
|
||||
apath="$out/$archive"
|
||||
rm -f "$apath"
|
||||
# Zip the staged files at the archive root (binaries + sapling params + asmap), excluding
|
||||
# the .gitkeep placeholder. The updater flattens paths via baseName(), so a flat zip is fine.
|
||||
files=()
|
||||
while IFS= read -r fn; do files+=("$fn"); done < <(cd "$d" && ls -A | grep -vx '.gitkeep')
|
||||
[ "${#files[@]}" -gt 0 ] || { echo "skip $plat: nothing to package in $d" >&2; continue; }
|
||||
( cd "$d" && zip -q -X "$apath" "${files[@]}" )
|
||||
sign_file "$key" "$apath"
|
||||
sum="$(sha256_of "$apath")"
|
||||
table+="| $archive | \`$sum\` |"$'\n'
|
||||
echo "packaged + signed: $apath (+ .sig) sha256=$sum"
|
||||
made=$((made + 1))
|
||||
done
|
||||
[ "$made" -gt 0 ] || die "no platform had a staged daemon binary under $src/dragonxd-{linux,mac,win}/"
|
||||
|
||||
echo
|
||||
echo "Checksum table (paste into the release body so the wallet can verify SHA-256):"
|
||||
echo "| Archive | SHA-256 |"
|
||||
echo "|---|---|"
|
||||
printf '%s' "$table"
|
||||
echo
|
||||
echo "Upload each .zip AND its .zip.sig as release assets. Wallet enforces the ed25519 signature"
|
||||
echo "(kDaemonRequireSignature=true) and the SHA-256 from the table above."
|
||||
;;
|
||||
|
||||
*)
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>... | release <secret.key> <version> [--src DIR] [--out DIR]}"
|
||||
;;
|
||||
esac
|
||||
|
||||
45
setup.sh
45
setup.sh
@@ -133,7 +133,7 @@ pkgs_core_arch="base-devel cmake git pkg-config
|
||||
libxkbcommon wayland libsodium curl
|
||||
autoconf automake libtool wget python xxd"
|
||||
|
||||
pkgs_core_macos="cmake python xxd"
|
||||
pkgs_core_macos="bash cmake python xxd"
|
||||
|
||||
# Windows cross-compile (from Linux)
|
||||
pkgs_win_debian="mingw-w64 zip"
|
||||
@@ -284,6 +284,14 @@ fi
|
||||
header "Windows Cross-Compile"
|
||||
|
||||
if $SETUP_WIN; then
|
||||
# Only touch apt / update-alternatives (which need sudo) when the toolchain is missing. If it is
|
||||
# already installed, skip them so `./setup.sh --win` can run WITHOUT sudo — important because the
|
||||
# daemon cross-compile that follows should run as the invoking user. Running the whole setup under
|
||||
# sudo leaves root-owned build artifacts under external/dragonx, which then break `make clean` on
|
||||
# a later non-sudo build (stale objects get relinked -> the mingw link failure recurs).
|
||||
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
|
||||
ok "Windows cross-compile toolchain already present — skipping apt install"
|
||||
else
|
||||
win_pkgs="$(get_pkgs win)"
|
||||
if [[ -n "$win_pkgs" ]]; then
|
||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
||||
@@ -298,6 +306,7 @@ if $SETUP_WIN; then
|
||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fetch libsodium for Windows
|
||||
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
|
||||
@@ -699,11 +708,13 @@ if [[ "$STALE_DAEMON" -eq 1 ]]; then
|
||||
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
|
||||
fi
|
||||
|
||||
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
||||
header "xmrig-hac Mining Binary"
|
||||
# ── 7. drg-xmrig (mining binary) ────────────────────────────────────────────
|
||||
header "drg-xmrig Mining Binary"
|
||||
|
||||
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
XMRIG_SRC="$PROJECT_DIR/external/drg-xmrig"
|
||||
# Output dir bundled by build.sh (Linux zip, AppImage, Windows embed, mac .app)
|
||||
# and scripts/legacy/build-windows.sh — keep this path in sync with those.
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
|
||||
# Clean previous prebuilt xmrig binaries so we always rebuild
|
||||
# Only clean the binary for the platform(s) we are actually building,
|
||||
@@ -715,14 +726,14 @@ if ! $CHECK_ONLY; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Helper: clone xmrig-hac if not present
|
||||
# Helper: clone drg-xmrig if not present
|
||||
clone_xmrig_if_needed() {
|
||||
if [[ ! -d "$XMRIG_SRC" ]]; then
|
||||
info "Cloning xmrig-hac..."
|
||||
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
|
||||
info "Cloning drg-xmrig..."
|
||||
git clone https://git.dragonx.is/DragonX/drg-xmrig.git "$XMRIG_SRC"
|
||||
else
|
||||
ok "xmrig-hac source already present"
|
||||
info "Pulling latest xmrig-hac..."
|
||||
ok "drg-xmrig source already present"
|
||||
info "Pulling latest drg-xmrig..."
|
||||
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
|
||||
fi
|
||||
}
|
||||
@@ -743,15 +754,15 @@ else
|
||||
rm -rf "$XMRIG_SRC/build"
|
||||
|
||||
# Build dependencies (libuv, hwloc, openssl)
|
||||
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..."
|
||||
info "Building drg-xmrig dependencies (libuv, hwloc, openssl)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
sh build_deps.sh
|
||||
)
|
||||
ok "xmrig-hac dependencies built"
|
||||
ok "drg-xmrig dependencies built"
|
||||
|
||||
# Build xmrig
|
||||
info "Building xmrig-hac (Linux)..."
|
||||
info "Building drg-xmrig (Linux)..."
|
||||
mkdir -p "$XMRIG_SRC/build"
|
||||
(
|
||||
cd "$XMRIG_SRC/build"
|
||||
@@ -768,7 +779,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
|
||||
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
else
|
||||
err "xmrig (Linux) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -792,7 +803,7 @@ else
|
||||
# Clean previous Windows build
|
||||
rm -rf "$XMRIG_SRC/build-windows"
|
||||
|
||||
info "Building xmrig-hac (Windows cross-compile)..."
|
||||
info "Building drg-xmrig (Windows cross-compile)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
bash build_windows.sh
|
||||
@@ -802,7 +813,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
|
||||
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
else
|
||||
err "xmrig.exe (Windows) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -812,7 +823,7 @@ fi
|
||||
# ── 8. Binary directories ───────────────────────────────────────────────────
|
||||
header "Binary Directories"
|
||||
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac drg-xmrig; do
|
||||
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
|
||||
if [[ -d "$dir" ]]; then
|
||||
# Count actual files (not .gitkeep)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "rpc/connection.h"
|
||||
#include "chat/chat_identity.h" // deriveChatIdentityFromSecret for HushChat identity provisioning
|
||||
#include "ui/windows/chat_tab.h" // ui::ResetChatTab — wipe chat UI plaintext on a wallet switch
|
||||
#include "ui/windows/mining_pool_panel.h" // ui::resolveMiningUserAddress
|
||||
#include <sodium.h> // sodium_memzero for wiping the fetched mnemonic
|
||||
#include <cctype>
|
||||
#include "config/settings.h"
|
||||
@@ -2288,27 +2289,23 @@ void App::startPoolMining(int threads)
|
||||
cfg.tls = settings_->getPoolTls();
|
||||
cfg.hugepages = settings_->getPoolHugepages();
|
||||
|
||||
// Use first shielded address as the mining wallet address, fall back to transparent
|
||||
// xmrig "user" is the pool login the block rewards are credited to. The user's
|
||||
// "Payout Address" field (cfg.worker_name = getPoolWorker) is exactly that, so it
|
||||
// takes priority — otherwise a payout address that differs from the wallet's own
|
||||
// first z-address is silently ignored and rewards go to the wrong address. Only when
|
||||
// no payout address is set do we fall back to the wallet's own first shielded, then
|
||||
// transparent, address (available even before the daemon is connected/synced).
|
||||
std::string firstShielded, firstTransparent;
|
||||
for (const auto& addr : state_.z_addresses) {
|
||||
if (!addr.address.empty()) {
|
||||
cfg.wallet_address = addr.address;
|
||||
break;
|
||||
if (!addr.address.empty()) { firstShielded = addr.address; break; }
|
||||
}
|
||||
}
|
||||
if (cfg.wallet_address.empty()) {
|
||||
for (const auto& addr : state_.addresses) {
|
||||
if (addr.type == "transparent" && !addr.address.empty()) {
|
||||
cfg.wallet_address = addr.address;
|
||||
firstTransparent = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use pool worker address from settings (available even before
|
||||
// the daemon is connected or the blockchain is synced).
|
||||
if (cfg.wallet_address.empty() && !cfg.worker_name.empty()) {
|
||||
cfg.wallet_address = cfg.worker_name;
|
||||
}
|
||||
cfg.wallet_address = ui::resolveMiningUserAddress(cfg.worker_name, firstShielded, firstTransparent);
|
||||
|
||||
if (cfg.wallet_address.empty()) {
|
||||
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
|
||||
// xmrig_manager.cpp — Pool mining process management via drg-xmrig.
|
||||
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
|
||||
|
||||
#include "xmrig_manager.h"
|
||||
|
||||
@@ -20,6 +20,17 @@ std::string defaultPoolWorkerAddress(const std::vector<AddressInfo>& addresses)
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string resolveMiningUserAddress(const std::string& payoutAddress,
|
||||
const std::string& firstShieldedAddress,
|
||||
const std::string& firstTransparentAddress)
|
||||
{
|
||||
// The configured payout address is the pool login rewards go to, so it wins over
|
||||
// the wallet's own addresses. "x" is the placeholder for an unset field.
|
||||
if (!payoutAddress.empty() && payoutAddress != "x") return payoutAddress;
|
||||
if (!firstShieldedAddress.empty()) return firstShieldedAddress;
|
||||
return firstTransparentAddress; // may be empty -> caller reports "no address"
|
||||
}
|
||||
|
||||
bool miningValueAlreadySaved(const std::vector<std::string>& savedValues,
|
||||
const std::string& value)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,14 @@ namespace ui {
|
||||
|
||||
bool shouldDefaultPoolWorker(const std::string& currentWorker, bool alreadyDefaulted);
|
||||
std::string defaultPoolWorkerAddress(const std::vector<AddressInfo>& addresses);
|
||||
|
||||
// The xmrig "user" — the pool login block rewards are credited to. The user-entered
|
||||
// payout address wins; otherwise fall back to the wallet's own first shielded, then
|
||||
// transparent, address. "x" is the empty-field placeholder and counts as unset. The
|
||||
// result may be empty (no address anywhere), which the caller treats as an error.
|
||||
std::string resolveMiningUserAddress(const std::string& payoutAddress,
|
||||
const std::string& firstShieldedAddress,
|
||||
const std::string& firstTransparentAddress);
|
||||
bool miningValueAlreadySaved(const std::vector<std::string>& savedValues,
|
||||
const std::string& value);
|
||||
const char* defaultPoolUrl();
|
||||
|
||||
@@ -251,11 +251,15 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
}
|
||||
y += gap * 0.5f;
|
||||
|
||||
// The pool list = official pools ∪ user-saved favorites ∪ the current custom pool.
|
||||
const auto effective = util::effectivePools(app->settings()->getPoolUrl(),
|
||||
app->settings()->getSavedPoolUrls());
|
||||
|
||||
// --- POOLS (N) header + Refresh ---
|
||||
{
|
||||
char hdr[48];
|
||||
snprintf(hdr, sizeof(hdr), "%s (%d)", TR("mining_pools_header"),
|
||||
(int)util::knownPools().size());
|
||||
(int)effective.size());
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), hdr);
|
||||
|
||||
float btnS = ovFont->LegacySize + 6 * dp;
|
||||
@@ -278,11 +282,11 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
{
|
||||
ImDrawList* cdl = ImGui::GetWindowDrawList();
|
||||
const auto snap = app->poolStatsSnapshot();
|
||||
const util::KnownPool* current = util::findKnownPoolByUrl(app->settings()->getPoolUrl());
|
||||
const util::KnownPool* current = util::findPoolByUrl(effective, app->settings()->getPoolUrl());
|
||||
const float childW = ImGui::GetContentRegionAvail().x;
|
||||
const float listRowH = capFont->LegacySize + 10 * dp;
|
||||
|
||||
for (const auto& kp : util::knownPools()) {
|
||||
for (const auto& kp : effective) {
|
||||
ImGui::PushID(kp.id.c_str());
|
||||
const bool isCurrent = current && current->id == kp.id;
|
||||
const auto it = snap.byId.find(kp.id);
|
||||
@@ -315,7 +319,17 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
|
||||
char right[64];
|
||||
std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string("—");
|
||||
snprintf(right, sizeof(right), "%s %.0f%% fee", hrStr.c_str(), kp.feePercent);
|
||||
// Prefer the live fee the pool reports; fall back to the compile-time
|
||||
// KnownPool.feePercent. A synthetic user pool has an unknown (<0) fee, so
|
||||
// we show just its hashrate placeholder for it.
|
||||
double feePct = (it != snap.byId.end() && it->second.feePercent >= 0.0)
|
||||
? it->second.feePercent
|
||||
: kp.feePercent;
|
||||
if (feePct >= 0.0)
|
||||
snprintf(right, sizeof(right), "%s %s%% fee", hrStr.c_str(),
|
||||
FormatFeePercent(feePct).c_str());
|
||||
else
|
||||
snprintf(right, sizeof(right), "%s", hrStr.c_str());
|
||||
ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right);
|
||||
cdl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rMax.x - rSz.x - 6 * dp, textY), OnSurfaceMedium(), right);
|
||||
|
||||
@@ -41,6 +41,21 @@ std::string FormatHashrate(double hashrate)
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
std::string FormatFeePercent(double feePercent)
|
||||
{
|
||||
// Whole fees read "1"; fractional ones keep only their significant decimals
|
||||
// ("1.5", "0.9", "1.25") with no trailing zeros. Capped at 2 dp — finer than
|
||||
// any pool advertises, and the caller appends the "%".
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%.2f", feePercent);
|
||||
std::string s(buffer);
|
||||
if (s.find('.') != std::string::npos) {
|
||||
s.erase(s.find_last_not_of('0') + 1);
|
||||
if (!s.empty() && s.back() == '.') s.pop_back();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty)
|
||||
{
|
||||
(void)difficulty;
|
||||
|
||||
@@ -9,6 +9,7 @@ int GetMaxMiningThreads();
|
||||
int ClampMiningThreads(int requestedThreads, int maxThreads);
|
||||
bool IsPoolMiningActive(bool poolMode, bool xmrigRunning, bool soloMiningRunning);
|
||||
std::string FormatHashrate(double hashrate);
|
||||
std::string FormatFeePercent(double feePercent);
|
||||
double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty);
|
||||
std::string FormatEstTime(double estimatedHours);
|
||||
|
||||
|
||||
@@ -45,16 +45,30 @@ struct PoolHashrate {
|
||||
std::string id;
|
||||
double hashrateHs = 0.0;
|
||||
bool ok = false;
|
||||
// Live pool fee (%) read from the same stats JSON. <0 means "not available" —
|
||||
// callers fall back to the compile-time KnownPool.feePercent.
|
||||
double feePercent = -1.0;
|
||||
};
|
||||
|
||||
// The built-in official pools (PPLNS only — never a SOLO pool, whose hashrate is
|
||||
// meaningless to balance against). Stable order.
|
||||
const std::vector<KnownPool>& knownPools();
|
||||
|
||||
// The known pool whose stratum matches `url` (host, and port when both specify one),
|
||||
// or nullptr. `url` may be a bare host, host:port, or carry a scheme/userinfo/path.
|
||||
// The pool in `pools` whose stratum matches `url` (host, and port when both specify
|
||||
// one), or nullptr. `url` may be a bare host, host:port, or carry a scheme/path.
|
||||
const KnownPool* findPoolByUrl(const std::vector<KnownPool>& pools, const std::string& url);
|
||||
|
||||
// Same, over the built-in official pools only.
|
||||
const KnownPool* findKnownPoolByUrl(const std::string& url);
|
||||
|
||||
// The full list the UI should show: the official knownPools(), plus a row for every
|
||||
// user-saved pool URL and for `currentPoolUrl` when it isn't one of those — so a
|
||||
// custom/bookmarked pool is a first-class, selectable row. Synthetic (user) rows are
|
||||
// official=false and carry no statsUrl (feePercent<0, no live hashrate), and endpoints
|
||||
// are de-duplicated so a saved URL that equals an official pool isn't listed twice.
|
||||
std::vector<KnownPool> effectivePools(const std::string& currentPoolUrl,
|
||||
const std::vector<std::string>& savedPoolUrls);
|
||||
|
||||
// The algo xmrig must use for `url`: the matching known pool's algo, else `fallback`.
|
||||
std::string resolvePoolAlgo(const std::string& url, const std::string& fallback);
|
||||
|
||||
@@ -64,6 +78,13 @@ std::string resolvePoolAlgo(const std::string& url, const std::string& fallback)
|
||||
double parsePoolHashrate(PoolStatsSchema schema, const std::string& json,
|
||||
const std::string& miningcorePoolId, bool& ok);
|
||||
|
||||
// Parse a pool's advertised fee (%) out of the same stats JSON (DragonXIs:
|
||||
// pools.<name>.poolFee; Miningcore: pools[id].poolFeePercent). Selects the same
|
||||
// pool entry as parsePoolHashrate. Sets ok=false and returns 0 when the field is
|
||||
// absent / malformed, so the caller keeps the compile-time fallback.
|
||||
double parsePoolFee(PoolStatsSchema schema, const std::string& json,
|
||||
const std::string& miningcorePoolId, bool& ok);
|
||||
|
||||
// Weighted-random pick among the usable (ok==true) pools: probability is inversely
|
||||
// proportional to hashrate (smaller pools favored), so miners spread out instead of
|
||||
// all stampeding to the single lowest pool. The current pool (`currentId`, may be
|
||||
|
||||
@@ -28,7 +28,7 @@ const std::vector<KnownPool>& knownPools()
|
||||
KnownPool{
|
||||
"dragonx-is", "pool.dragonx.is", "pool.dragonx.is:3433", "rx/hush",
|
||||
"https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs,
|
||||
/*miningcorePoolId=*/"", /*feePercent=*/0.0, /*official=*/true,
|
||||
/*miningcorePoolId=*/"", /*feePercent=*/1.0, /*official=*/true,
|
||||
},
|
||||
};
|
||||
return pools;
|
||||
@@ -83,13 +83,62 @@ bool sameEndpoint(const std::string& a, const std::string& b)
|
||||
return pa == pb;
|
||||
}
|
||||
|
||||
// Build a synthetic, selectable pool row for a user-supplied URL (a saved favorite
|
||||
// or the current custom pool). We don't know its stats API, so it carries no
|
||||
// statsUrl / live hashrate and an unknown (<0) fee — the UI falls back to "—".
|
||||
KnownPool makeUserPool(const std::string& url)
|
||||
{
|
||||
KnownPool p;
|
||||
const std::string hp = hostPortOf(url);
|
||||
std::string host, port;
|
||||
splitHostPort(hp, host, port);
|
||||
p.id = "user:" + trimmed(url); // stable + unique (used as the ImGui id)
|
||||
p.label = host.empty() ? hp : host;
|
||||
p.stratum = trimmed(url); // what the miner connects to / a row-click restores
|
||||
p.algo = ""; // unknown; xmrig resolves via resolvePoolAlgo's fallback
|
||||
p.statsUrl = ""; // no known stats endpoint -> no live hashrate/fee
|
||||
p.schema = PoolStatsSchema::DragonXIs;
|
||||
p.miningcorePoolId = "";
|
||||
p.feePercent = -1.0; // unknown fee
|
||||
p.official = false;
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const KnownPool* findPoolByUrl(const std::vector<KnownPool>& pools, const std::string& url)
|
||||
{
|
||||
for (const auto& p : pools)
|
||||
if (sameEndpoint(p.stratum, url)) return &p;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const KnownPool* findKnownPoolByUrl(const std::string& url)
|
||||
{
|
||||
for (const auto& p : knownPools())
|
||||
if (sameEndpoint(p.stratum, url)) return &p;
|
||||
return nullptr;
|
||||
return findPoolByUrl(knownPools(), url);
|
||||
}
|
||||
|
||||
std::vector<KnownPool> effectivePools(const std::string& currentPoolUrl,
|
||||
const std::vector<std::string>& savedPoolUrls)
|
||||
{
|
||||
std::vector<KnownPool> pools = knownPools();
|
||||
|
||||
// Skip anything whose endpoint already appears (official or an earlier user row).
|
||||
auto listed = [&](const std::string& url) {
|
||||
return findPoolByUrl(pools, url) != nullptr;
|
||||
};
|
||||
|
||||
for (const auto& url : savedPoolUrls) {
|
||||
if (trimmed(url).empty() || listed(url)) continue;
|
||||
pools.push_back(makeUserPool(url));
|
||||
}
|
||||
|
||||
// The pool currently being mined, if not already shown, so the active pool is
|
||||
// always visible even before it's bookmarked.
|
||||
if (!trimmed(currentPoolUrl).empty() && !listed(currentPoolUrl))
|
||||
pools.push_back(makeUserPool(currentPoolUrl));
|
||||
|
||||
return pools;
|
||||
}
|
||||
|
||||
std::string resolvePoolAlgo(const std::string& url, const std::string& fallback)
|
||||
@@ -159,6 +208,64 @@ double parsePoolHashrate(PoolStatsSchema schema, const std::string& jsonStr,
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double parsePoolFee(PoolStatsSchema schema, const std::string& jsonStr,
|
||||
const std::string& miningcorePoolId, bool& ok)
|
||||
{
|
||||
ok = false;
|
||||
try {
|
||||
const json j = json::parse(jsonStr);
|
||||
|
||||
if (schema == PoolStatsSchema::DragonXIs) {
|
||||
// { "pools": { "dragonx": { "poolFee": <num>, ... }, ... } }
|
||||
if (j.contains("pools") && j["pools"].is_object()) {
|
||||
const auto& pools = j["pools"];
|
||||
auto readFee = [&](const json& pool, double& out) -> bool {
|
||||
if (pool.is_object() && pool.contains("poolFee") &&
|
||||
pool["poolFee"].is_number()) {
|
||||
out = pool["poolFee"].get<double>();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
double fee = 0.0;
|
||||
if (pools.contains("dragonx") && readFee(pools["dragonx"], fee)) {
|
||||
ok = true;
|
||||
return fee;
|
||||
}
|
||||
for (auto it = pools.begin(); it != pools.end(); ++it) {
|
||||
if (readFee(it.value(), fee)) {
|
||||
ok = true;
|
||||
return fee;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // Miningcore: pools[id].poolFeePercent
|
||||
if (j.contains("pools") && j["pools"].is_array()) {
|
||||
const json* chosen = nullptr;
|
||||
for (const auto& pool : j["pools"]) {
|
||||
if (!pool.is_object()) continue;
|
||||
if (!miningcorePoolId.empty()) {
|
||||
if (pool.value("id", std::string{}) == miningcorePoolId) {
|
||||
chosen = &pool;
|
||||
break;
|
||||
}
|
||||
} else if (!chosen) {
|
||||
chosen = &pool; // first pool when no id requested
|
||||
}
|
||||
}
|
||||
if (chosen && chosen->contains("poolFeePercent") &&
|
||||
(*chosen)["poolFeePercent"].is_number()) {
|
||||
ok = true;
|
||||
return (*chosen)["poolFeePercent"].get<double>();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
// fall through — ok stays false
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
std::string chooseWeightedPool(const std::vector<PoolHashrate>& pools,
|
||||
const std::string& currentId,
|
||||
std::mt19937& rng)
|
||||
|
||||
@@ -94,6 +94,12 @@ void PoolStatsService::run(std::vector<KnownPool> pools)
|
||||
const double v = parsePoolHashrate(p.schema, body, p.miningcorePoolId, ok);
|
||||
hr.ok = ok;
|
||||
hr.hashrateHs = ok ? v : 0.0;
|
||||
|
||||
bool feeOk = false;
|
||||
const double fee = parsePoolFee(p.schema, body, p.miningcorePoolId, feeOk);
|
||||
// Only trust a sane fee; anything else leaves feePercent < 0 so the UI
|
||||
// falls back to the compile-time KnownPool.feePercent.
|
||||
if (feeOk && fee >= 0.0 && fee <= 100.0) hr.feePercent = fee;
|
||||
}
|
||||
results[p.id] = hr;
|
||||
}
|
||||
|
||||
@@ -3231,6 +3231,19 @@ void testRendererHelpers()
|
||||
EXPECT_EQ(dragonx::ui::defaultPoolWorkerAddress(poolAddresses), std::string("zs-default-worker"));
|
||||
EXPECT_TRUE(dragonx::ui::miningValueAlreadySaved({"pool-a", "pool-b"}, "pool-b"));
|
||||
EXPECT_FALSE(dragonx::ui::miningValueAlreadySaved({"pool-a"}, ""));
|
||||
|
||||
// resolveMiningUserAddress: the configured payout address is the xmrig "user"
|
||||
// (where rewards go) and must win over the wallet's own addresses.
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("zs-payout", "zs-own", "R-own"),
|
||||
std::string("zs-payout")); // explicit payout wins
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("", "zs-own", "R-own"),
|
||||
std::string("zs-own")); // unset -> own shielded
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("x", "zs-own", "R-own"),
|
||||
std::string("zs-own")); // "x" placeholder counts as unset
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("x", "", "R-own"),
|
||||
std::string("R-own")); // no shielded -> transparent
|
||||
EXPECT_EQ(dragonx::ui::resolveMiningUserAddress("", "", ""),
|
||||
std::string("")); // nothing anywhere -> caller errors
|
||||
EXPECT_EQ(std::string(dragonx::ui::defaultPoolUrl()), std::string("pool.dragonx.is:3433"));
|
||||
|
||||
dragonx::TransactionInfo tx;
|
||||
@@ -5828,6 +5841,94 @@ void testPoolHashrateParsing()
|
||||
EXPECT_FALSE(ok);
|
||||
}
|
||||
|
||||
// Schema-aware pool fee parsing (fed to the mining-tab "N% fee" display).
|
||||
void testPoolFeeParsing()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
bool ok = false;
|
||||
|
||||
// pool.dragonx.is custom schema: pools.dragonx.poolFee (a whole-percent number).
|
||||
const std::string isJson =
|
||||
R"({"pools":{"dragonx":{"hashrate":27670.14,"poolFee":1,"soloFee":3}}})";
|
||||
double fee = parsePoolFee(PoolStatsSchema::DragonXIs, isJson, "", ok);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_NEAR(fee, 1.0, 0.001);
|
||||
|
||||
// Fractional fees survive (display rounds, but the parse must not).
|
||||
const std::string isFrac = R"({"pools":{"dragonx":{"poolFee":1.5}}})";
|
||||
fee = parsePoolFee(PoolStatsSchema::DragonXIs, isFrac, "", ok);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_NEAR(fee, 1.5, 0.001);
|
||||
|
||||
// Miningcore schema: the requested pool id's poolFeePercent.
|
||||
const std::string ccJson =
|
||||
R"({"pools":[)"
|
||||
R"({"id":"dragonx-solo","poolFeePercent":2.0,"poolStats":{"poolHashrate":88780.0}},)"
|
||||
R"({"id":"dragonx-pplns","poolFeePercent":0.9,"poolStats":{"poolHashrate":1585.9}}]})";
|
||||
fee = parsePoolFee(PoolStatsSchema::Miningcore, ccJson, "dragonx-pplns", ok);
|
||||
EXPECT_TRUE(ok);
|
||||
EXPECT_NEAR(fee, 0.9, 0.001);
|
||||
|
||||
// Missing field / malformed / wrong-schema input all fail closed (caller keeps
|
||||
// the compile-time fallback rather than showing a bogus 0%).
|
||||
parsePoolFee(PoolStatsSchema::DragonXIs, R"({"pools":{"dragonx":{"hashrate":1.0}}})", "", ok);
|
||||
EXPECT_FALSE(ok); // no poolFee key
|
||||
parsePoolFee(PoolStatsSchema::DragonXIs, "not json", "", ok);
|
||||
EXPECT_FALSE(ok);
|
||||
parsePoolFee(PoolStatsSchema::Miningcore, ccJson, "does-not-exist", ok);
|
||||
EXPECT_FALSE(ok);
|
||||
parsePoolFee(PoolStatsSchema::DragonXIs, R"({"pools":{"dragonx":{"poolFee":"1"}}})", "", ok);
|
||||
EXPECT_FALSE(ok); // string, not number
|
||||
}
|
||||
|
||||
// The effective pool list = official pools ∪ saved favorites ∪ current custom pool,
|
||||
// endpoint-deduped, with synthetic user rows flagged official=false.
|
||||
void testEffectivePools()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
const int base = (int)knownPools().size();
|
||||
|
||||
// Current pool is the official one, nothing saved -> just the official pools.
|
||||
auto a = effectivePools("pool.dragonx.is:3433", {});
|
||||
EXPECT_EQ((int)a.size(), base);
|
||||
|
||||
// A custom current pool (neither official nor saved) appears as an extra row.
|
||||
auto b = effectivePools("my.pool.example:3333", {});
|
||||
EXPECT_EQ((int)b.size(), base + 1);
|
||||
const KnownPool* custom = findPoolByUrl(b, "my.pool.example:3333");
|
||||
EXPECT_TRUE(custom != nullptr);
|
||||
EXPECT_FALSE(custom->official);
|
||||
EXPECT_TRUE(custom->feePercent < 0.0); // unknown fee
|
||||
|
||||
// Saved pools are appended; an official one among them and a duplicate collapse.
|
||||
auto c = effectivePools("pool.dragonx.is:3433",
|
||||
{"pool.dragonx.is:3433", "alt.pool:1", "alt.pool:1"});
|
||||
EXPECT_EQ((int)c.size(), base + 1);
|
||||
EXPECT_TRUE(findPoolByUrl(c, "alt.pool:1") != nullptr);
|
||||
|
||||
// Current pool equal to a saved one is not listed twice.
|
||||
auto d = effectivePools("alt.pool:1", {"alt.pool:1"});
|
||||
EXPECT_EQ((int)d.size(), base + 1);
|
||||
|
||||
// Blank/whitespace URLs are ignored (no phantom rows).
|
||||
auto e = effectivePools(" ", {"", " "});
|
||||
EXPECT_EQ((int)e.size(), base);
|
||||
}
|
||||
|
||||
// Fee formatting: whole numbers stay clean, fractional fees keep their decimals.
|
||||
void testFormatFeePercent()
|
||||
{
|
||||
using dragonx::ui::FormatFeePercent;
|
||||
EXPECT_TRUE(FormatFeePercent(1.0) == "1");
|
||||
EXPECT_TRUE(FormatFeePercent(0.0) == "0");
|
||||
EXPECT_TRUE(FormatFeePercent(3.0) == "3");
|
||||
EXPECT_TRUE(FormatFeePercent(1.5) == "1.5");
|
||||
EXPECT_TRUE(FormatFeePercent(0.9) == "0.9");
|
||||
EXPECT_TRUE(FormatFeePercent(1.25) == "1.25");
|
||||
EXPECT_TRUE(FormatFeePercent(2.50) == "2.5"); // trailing zero trimmed
|
||||
EXPECT_TRUE(FormatFeePercent(100.0) == "100");
|
||||
}
|
||||
|
||||
// Weighted-random pool selection: smaller pools favored, incumbent sticky, fails safe.
|
||||
void testPoolWeightedSelection()
|
||||
{
|
||||
@@ -6590,6 +6691,9 @@ int main()
|
||||
testLiteOfficialServerDetection();
|
||||
testPoolRegistryLookup();
|
||||
testPoolHashrateParsing();
|
||||
testPoolFeeParsing();
|
||||
testEffectivePools();
|
||||
testFormatFeePercent();
|
||||
testPoolWeightedSelection();
|
||||
testAtomicFileWrite();
|
||||
testHushChatCrypto();
|
||||
|
||||
12
third_party/silentdragonxlite/lib/rust-toolchain.toml
vendored
Normal file
12
third_party/silentdragonxlite/lib/rust-toolchain.toml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Rust toolchain pin for the vendored SilentDragonXLite (SDXL) backend.
|
||||
#
|
||||
# The pinned librustzcash + transitive crates (notably traitobject 0.1.0) rely on
|
||||
# pre-1.70 trait-coherence rules and fail to compile on newer rustc with
|
||||
# error[E0119]: conflicting implementations of trait `Trait` for type `(dyn Send + Sync)`
|
||||
# so the backend must be built with 1.63 (the toolchain scripts/build-lite-backend-artifact.sh
|
||||
# and CLAUDE.md target). rustup auto-selects this when cargo runs in this tree, so no
|
||||
# RUSTUP_TOOLCHAIN / `cargo +1.63.0` is needed.
|
||||
#
|
||||
# Install it once with: rustup toolchain install 1.63.0
|
||||
[toolchain]
|
||||
channel = "1.63.0"
|
||||
Reference in New Issue
Block a user