feat(lite): lite wallet foundation (inherited working-tree state)

Preserve the previously-uncommitted lite wallet implementation and related dev WIP
under version control:
- src/wallet/ lite services: client bridge, bridge runtime, connection, lifecycle,
  sync, gateway, result parsers, state mapper, artifact contract/resolver, refresh
  services, UI adapters, wallet_backend/capabilities. (Includes two small M1 fixes:
  lifecycle walletReady now parses the response; default chain name -> "main".)
- src/chat/ chat protocol; tests/fixtures/ (lite + hushchat); tools/hushchat_fixture_check.cpp;
  scripts/build-lite-backend-artifact.sh.
- Pre-existing modified app_network/security/wizard, network_refresh_service, sidebar,
  mining_tab, bootstrap dialog, and version headers captured as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 21:15:28 -05:00
parent a78a13edf3
commit 863d015628
69 changed files with 39458 additions and 85 deletions

View File

@@ -0,0 +1,769 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ABI_VERSION="sdxl-c-v1"
LINK_MODE="imported"
BACKEND_DIR="$PROJECT_ROOT/external/SilentDragonXLite/lib"
BACKEND_SOURCE_DIR=""
BUILD_BACKEND_DIR=""
BACKEND_DEPENDENCY_DIR=""
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=false
OUT_DIR="$PROJECT_ROOT/build/lite-backend"
PLATFORM=""
RUST_TARGET=""
CARGO_TARGET_DIR_VALUE="${CARGO_TARGET_DIR:-}"
ARTIFACT_PATH=""
BUILD_ARTIFACT=true
BUILDER="${DRAGONX_LITE_BACKEND_BUILDER:-local}"
JOBS="${JOBS:-}"
SOURCE_DATE_EPOCH_VALUE="${SOURCE_DATE_EPOCH:-}"
REPRODUCIBLE=false
SIGNATURE_REQUIRED=false
SIGNATURE_FILE=""
SIGNATURE_FORMAT=""
SIGNATURE_VERIFICATION_TOOL=""
SIGNATURE_VERIFICATION_COMMAND=""
SIGNATURE_KEY_FINGERPRINT=""
SIGNATURE_CERTIFICATE_IDENTITY=""
SIGNATURE_CERTIFICATE_ISSUER=""
SIGNATURE_TRANSPARENCY_LOG_URL=""
SIGNATURE_VERIFIED_SHA256=""
SIGNATURE_POLICY_NAME="dragonx-lite-backend-signature-policy-v1"
SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE=true
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
SIGNATURE_METADATA_PROVIDED=false
SIGNATURE_VERIFICATION_PERFORMED=false
SIGNATURE_VERIFICATION_STATUS="not-provided"
SIGNATURE_FILE_SHA256=""
REQUIRED_SYMBOLS=(
litelib_wallet_exists
litelib_initialize_new
litelib_initialize_new_from_phrase
litelib_initialize_existing
litelib_execute
litelib_rust_free_string
litelib_check_server_online
litelib_shutdown
)
EXTRA_CARGO_ARGS=()
EXTRA_REMAP_PATH_PREFIXES=()
usage() {
cat <<EOF
Build or inventory the SDXL-compatible lite backend artifact.
Usage: $0 [options]
Options:
--platform linux|windows|macos Artifact platform. Defaults to host platform.
--rust-target TRIPLE Cargo target triple for cross builds.
--cargo-target-dir PATH Isolated Cargo target directory for clean builds.
--backend-dir PATH SilentDragonXLite/lib source directory.
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
--out-dir PATH Output directory for copied artifact and metadata.
--artifact PATH Inventory an existing artifact instead of building.
--no-build Do not run cargo; requires --artifact.
--reproducible Add deterministic Rust path remaps for clean builds.
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
--builder NAME Redacted builder/provenance label. Default: local.
--signature-required Fail if verified signature metadata is not supplied.
--signature-file PATH Existing sidecar signature file to record.
--signature-format FORMAT Signature format: minisign, gpg, sigstore, external, or other.
--signature-verification-tool T Verification tool and version used by the release builder.
--signature-verification-command C
Verification command already run by the release builder.
--signature-key-fingerprint F Reviewed public-key fingerprint, when applicable.
--signature-certificate-identity ID
Reviewed certificate identity, when applicable.
--signature-certificate-issuer I
Reviewed certificate issuer, when applicable.
--signature-transparency-log-url URL
Transparency log entry, when applicable.
--signature-verified-sha256 SHA Artifact SHA-256 verified by the signature check.
-j, --jobs N Cargo parallel jobs.
--cargo-arg ARG Extra argument forwarded to cargo build.
-h, --help Show this help.
Outputs:
<out>/<platform>/<artifact>
<out>/<platform>/lite-backend-symbols.txt
<out>/<platform>/lite-backend-artifact-manifest.json
The script captures symbols, checksums, and optional read-only signature
verification metadata only. It does not load the library, resolve function
pointers, call SDXL, sign, upload, or publish artifacts.
EOF
}
info() { printf '[lite-backend] %s\n' "$*"; }
warn() { printf '[lite-backend] warning: %s\n' "$*" >&2; }
die() { printf '[lite-backend] ERROR: %s\n' "$*" >&2; exit 1; }
absolute_path() {
local path="$1"
if [[ "$path" = /* ]]; then
printf '%s\n' "$path"
else
printf '%s/%s\n' "$PWD" "$path"
fi
}
host_platform() {
case "$(uname -s)" in
Linux) printf 'linux\n' ;;
Darwin) printf 'macos\n' ;;
MINGW*|MSYS*|CYGWIN*) printf 'windows\n' ;;
*) die "unsupported host platform: $(uname -s)" ;;
esac
}
normalize_platform() {
case "$1" in
linux|Linux) printf 'linux\n' ;;
windows|win|Win|Windows) printf 'windows\n' ;;
macos|mac|darwin|Darwin) printf 'macos\n' ;;
"") host_platform ;;
*) die "unsupported platform: $1" ;;
esac
}
while [[ $# -gt 0 ]]; do
case "$1" in
--platform)
[[ $# -ge 2 ]] || die "--platform requires a value"
PLATFORM="$(normalize_platform "$2")"
shift 2
;;
--rust-target)
[[ $# -ge 2 ]] || die "--rust-target requires a value"
RUST_TARGET="$2"
shift 2
;;
--cargo-target-dir)
[[ $# -ge 2 ]] || die "--cargo-target-dir requires a value"
CARGO_TARGET_DIR_VALUE="$(absolute_path "$2")"
shift 2
;;
--backend-dir)
[[ $# -ge 2 ]] || die "--backend-dir requires a value"
BACKEND_DIR="$(absolute_path "$2")"
shift 2
;;
--silentdragonxlitelib-dir)
[[ $# -ge 2 ]] || die "--silentdragonxlitelib-dir requires a value"
BACKEND_DEPENDENCY_DIR="$(absolute_path "$2")"
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=true
shift 2
;;
--out-dir)
[[ $# -ge 2 ]] || die "--out-dir requires a value"
OUT_DIR="$(absolute_path "$2")"
shift 2
;;
--artifact)
[[ $# -ge 2 ]] || die "--artifact requires a value"
ARTIFACT_PATH="$(absolute_path "$2")"
BUILD_ARTIFACT=false
shift 2
;;
--no-build)
BUILD_ARTIFACT=false
shift
;;
--reproducible)
REPRODUCIBLE=true
shift
;;
--remap-path-prefix)
[[ $# -ge 2 ]] || die "--remap-path-prefix requires FROM=TO"
[[ "$2" == *=* ]] || die "--remap-path-prefix requires FROM=TO"
EXTRA_REMAP_PATH_PREFIXES+=("$2")
shift 2
;;
--builder)
[[ $# -ge 2 ]] || die "--builder requires a value"
BUILDER="$2"
shift 2
;;
--signature-required)
SIGNATURE_REQUIRED=true
shift
;;
--signature-file|--signature-path)
[[ $# -ge 2 ]] || die "$1 requires a value"
SIGNATURE_FILE="$(absolute_path "$2")"
shift 2
;;
--signature-format)
[[ $# -ge 2 ]] || die "--signature-format requires a value"
SIGNATURE_FORMAT="$2"
shift 2
;;
--signature-verification-tool|--signature-tool)
[[ $# -ge 2 ]] || die "$1 requires a value"
SIGNATURE_VERIFICATION_TOOL="$2"
shift 2
;;
--signature-verification-command)
[[ $# -ge 2 ]] || die "--signature-verification-command requires a value"
SIGNATURE_VERIFICATION_COMMAND="$2"
shift 2
;;
--signature-key-fingerprint)
[[ $# -ge 2 ]] || die "--signature-key-fingerprint requires a value"
SIGNATURE_KEY_FINGERPRINT="$2"
shift 2
;;
--signature-certificate-identity)
[[ $# -ge 2 ]] || die "--signature-certificate-identity requires a value"
SIGNATURE_CERTIFICATE_IDENTITY="$2"
shift 2
;;
--signature-certificate-issuer)
[[ $# -ge 2 ]] || die "--signature-certificate-issuer requires a value"
SIGNATURE_CERTIFICATE_ISSUER="$2"
shift 2
;;
--signature-transparency-log-url)
[[ $# -ge 2 ]] || die "--signature-transparency-log-url requires a value"
SIGNATURE_TRANSPARENCY_LOG_URL="$2"
shift 2
;;
--signature-verified-sha256)
[[ $# -ge 2 ]] || die "--signature-verified-sha256 requires a value"
SIGNATURE_VERIFIED_SHA256="$2"
shift 2
;;
-j|--jobs)
[[ $# -ge 2 ]] || die "--jobs requires a value"
JOBS="$2"
shift 2
;;
--cargo-arg)
[[ $# -ge 2 ]] || die "--cargo-arg requires a value"
EXTRA_CARGO_ARGS+=("$2")
shift 2
;;
-h|--help)
usage
exit 0
;;
*) die "unknown option: $1" ;;
esac
done
PLATFORM="$(normalize_platform "$PLATFORM")"
BACKEND_SOURCE_DIR="$BACKEND_DIR"
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
if [[ "$PLATFORM" == "windows" && -z "$RUST_TARGET" ]]; then
RUST_TARGET="x86_64-pc-windows-gnu"
fi
if [[ "$PLATFORM" == "macos" && -z "$RUST_TARGET" && "$(host_platform)" != "macos" ]]; then
die "macOS artifacts require --rust-target when not running on macOS"
fi
if [[ "$BUILD_ARTIFACT" == false && -z "$ARTIFACT_PATH" ]]; then
die "--no-build requires --artifact"
fi
backend_dependency_path_from_cargo() {
local cargo_toml="$1"
awk '
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
original = $0
path = $0
sub(/.*path[[:space:]]*=[[:space:]]*"/, "", path)
sub(/".*/, "", path)
if (path != original) print path
exit
}
' "$cargo_toml"
}
canonical_dependency_path() {
local path="$1"
if [[ -d "$path" ]]; then
(cd "$path" && pwd -P)
else
absolute_path "$path"
fi
}
validate_backend_dependency_source() {
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] || return
if [[ ! -f "$BACKEND_DEPENDENCY_DIR/Cargo.toml" ]]; then
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
die "Cargo.toml not found in $BACKEND_DEPENDENCY_DIR"
fi
warn "Cargo.toml not found in silentdragonxlitelib source: $BACKEND_DEPENDENCY_DIR"
return
fi
if ! grep -Eq '^[[:space:]]*name[[:space:]]*=[[:space:]]*"silentdragonxlitelib"' "$BACKEND_DEPENDENCY_DIR/Cargo.toml"; then
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
die "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
fi
warn "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
fi
}
prepare_backend_source() {
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == false ]]; then
if [[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]]; then
local configured_dependency_path
configured_dependency_path="$(backend_dependency_path_from_cargo "$BACKEND_SOURCE_DIR/Cargo.toml")"
if [[ -n "$configured_dependency_path" ]]; then
if [[ "$configured_dependency_path" = /* ]]; then
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$configured_dependency_path")"
warn "backend Cargo.toml uses an absolute silentdragonxlitelib path; use --silentdragonxlitelib-dir for portable builders"
else
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$BACKEND_SOURCE_DIR/$configured_dependency_path")"
info "using relative silentdragonxlitelib dependency at $BACKEND_DEPENDENCY_DIR"
fi
validate_backend_dependency_source
fi
fi
return
fi
[[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BACKEND_SOURCE_DIR"
validate_backend_dependency_source
[[ "$BACKEND_DEPENDENCY_DIR" != *\"* ]] || die "--silentdragonxlitelib-dir path cannot contain a double quote"
local prepared_root="$OUT_DIR/.prepared-backend/$PLATFORM"
[[ "$prepared_root" == */.prepared-backend/* ]] || die "refusing unsafe prepared backend path: $prepared_root"
rm -rf "$prepared_root"
mkdir -p "$prepared_root"
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"
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
[[ -f "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" ]] && ln -s "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" "$prepared_root/silentdragonxlitelib.h"
local replacement="silentdragonxlitelib = { path = \"$BACKEND_DEPENDENCY_DIR\" }"
awk -v replacement="$replacement" '
BEGIN { replaced = 0 }
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
print replacement
replaced = 1
next
}
{ print }
END { if (replaced != 1) exit 42 }
' "$BACKEND_SOURCE_DIR/Cargo.toml" > "$prepared_root/Cargo.toml" \
|| die "failed to prepare backend Cargo.toml with portable silentdragonxlitelib path"
BUILD_BACKEND_DIR="$prepared_root"
info "prepared backend source at $BUILD_BACKEND_DIR with silentdragonxlitelib from $BACKEND_DEPENDENCY_DIR"
}
prepare_backend_source
artifact_kind() {
local name="${1##*/}"
case "$name" in
*.a|*.lib) printf 'static-library\n' ;;
*.so|*.dylib|*.dll) printf 'shared-library\n' ;;
*) printf 'unknown\n' ;;
esac
}
cargo_output_candidates() {
local cargo_target_root="$BUILD_BACKEND_DIR/target"
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
fi
local base="$cargo_target_root/release"
if [[ -n "$RUST_TARGET" ]]; then
base="$cargo_target_root/$RUST_TARGET/release"
fi
case "$PLATFORM" in
linux)
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.so"
;;
windows)
printf '%s\n' "$base/silentdragonxlite.lib" "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.dll"
;;
macos)
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.dylib" "$base/silentdragonxlite.dylib"
;;
esac
}
source_revision_for() {
local dir="$1"
local revision_file
for revision_file in "$dir/DRAGONX_SOURCE_REVISION" "$dir/../DRAGONX_SOURCE_REVISION"; do
if [[ -f "$revision_file" ]]; then
sed -n '1p' "$revision_file"
return
fi
done
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$dir" rev-parse HEAD 2>/dev/null || printf 'unknown'
else
printf 'unknown'
fi
}
default_source_date_epoch() {
if git -C "$PROJECT_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$PROJECT_ROOT" log -1 --format=%ct 2>/dev/null || printf '0'
else
printf '0'
fi
}
append_rustflag() {
local rustflag="$1"
if [[ -n "${RUSTFLAGS:-}" ]]; then
export RUSTFLAGS="${RUSTFLAGS} ${rustflag}"
else
export RUSTFLAGS="$rustflag"
fi
}
append_rust_path_remap() {
local from_path="$1"
local to_path="$2"
[[ -n "$from_path" && -n "$to_path" ]] || return
append_rustflag "--remap-path-prefix=${from_path}=${to_path}"
}
apply_reproducible_rustflags() {
local cargo_target_root="$BUILD_BACKEND_DIR/target"
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
fi
append_rust_path_remap "$PROJECT_ROOT" "/dragonx-project"
append_rust_path_remap "$BACKEND_SOURCE_DIR" "/dragonx-lite-backend"
if [[ "$BUILD_BACKEND_DIR" != "$BACKEND_SOURCE_DIR" ]]; then
append_rust_path_remap "$BUILD_BACKEND_DIR" "/dragonx-lite-backend"
fi
append_rust_path_remap "$BACKEND_DEPENDENCY_DIR" "/dragonx-lite-backend-dependency"
for path_remap in "${EXTRA_REMAP_PATH_PREFIXES[@]}"; do
append_rustflag "--remap-path-prefix=${path_remap}"
done
local cargo_home="${CARGO_HOME:-}"
if [[ -z "$cargo_home" && -n "${HOME:-}" ]]; then
cargo_home="$HOME/.cargo"
fi
if [[ -n "$cargo_home" && -d "$cargo_home" ]]; then
append_rust_path_remap "$cargo_home" "/cargo-home"
fi
append_rust_path_remap "$cargo_target_root" "/dragonx-lite-cargo-target"
}
build_with_cargo() {
command -v cargo >/dev/null 2>&1 || die "cargo was not found"
[[ -f "$BUILD_BACKEND_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BUILD_BACKEND_DIR"
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
fi
export CARGO_INCREMENTAL=0
export SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH_VALUE"
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
export CARGO_TARGET_DIR="$CARGO_TARGET_DIR_VALUE"
fi
if [[ "$REPRODUCIBLE" == true ]]; then
apply_reproducible_rustflags
fi
if [[ "$PLATFORM" == "windows" && -d "$BUILD_BACKEND_DIR/libsodium-mingw" ]]; then
export SODIUM_LIB_DIR="$BUILD_BACKEND_DIR/libsodium-mingw"
fi
local cargo_cmd=(cargo build --locked --lib --release)
if [[ -n "$RUST_TARGET" ]]; then
cargo_cmd+=(--target "$RUST_TARGET")
fi
if [[ -n "$JOBS" ]]; then
cargo_cmd+=(-j "$JOBS")
fi
cargo_cmd+=("${EXTRA_CARGO_ARGS[@]}")
info "building backend in $BUILD_BACKEND_DIR"
(cd "$BUILD_BACKEND_DIR" && "${cargo_cmd[@]}")
while IFS= read -r candidate; do
if [[ -f "$candidate" ]]; then
ARTIFACT_PATH="$candidate"
return
fi
done < <(cargo_output_candidates)
die "cargo finished, but no expected backend artifact was found under $BUILD_BACKEND_DIR/target"
}
select_nm_tool() {
if [[ "$PLATFORM" == "windows" ]] && command -v x86_64-w64-mingw32-nm >/dev/null 2>&1; then
printf 'x86_64-w64-mingw32-nm\n'
return
fi
if command -v llvm-nm >/dev/null 2>&1; then
printf 'llvm-nm\n'
return
fi
if command -v nm >/dev/null 2>&1; then
printf 'nm\n'
return
fi
die "no symbol inventory tool found; install nm, llvm-nm, or x86_64-w64-mingw32-nm"
}
compute_sha256() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
else
die "sha256sum or shasum is required"
fi
}
json_escape() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
value="${value//$'\n'/\\n}"
value="${value//$'\r'/}"
value="${value//$'\t'/\\t}"
printf '"%s"' "$value"
}
json_array() {
local first=true
printf '['
for value in "$@"; do
if [[ "$first" == true ]]; then
first=false
else
printf ','
fi
json_escape "$value"
done
printf ']'
}
json_array_from_file() {
local file="$1"
local values=()
if [[ -f "$file" ]]; then
mapfile -t values < "$file"
fi
json_array "${values[@]}"
}
signature_metadata_requested() {
[[ "$SIGNATURE_REQUIRED" == true || \
-n "$SIGNATURE_FILE" || \
-n "$SIGNATURE_FORMAT" || \
-n "$SIGNATURE_VERIFICATION_TOOL" || \
-n "$SIGNATURE_VERIFICATION_COMMAND" || \
-n "$SIGNATURE_KEY_FINGERPRINT" || \
-n "$SIGNATURE_CERTIFICATE_IDENTITY" || \
-n "$SIGNATURE_CERTIFICATE_ISSUER" || \
-n "$SIGNATURE_TRANSPARENCY_LOG_URL" || \
-n "$SIGNATURE_VERIFIED_SHA256" ]]
}
validate_signature_metadata() {
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
if [[ "$SIGNATURE_REQUIRED" == true ]]; then
SIGNATURE_REQUIRED_MANIFEST_VALUE=true
fi
if ! signature_metadata_requested; then
return
fi
[[ -n "$SIGNATURE_FILE" ]] || die "signature metadata requires --signature-file"
[[ -f "$SIGNATURE_FILE" ]] || die "signature file does not exist: $SIGNATURE_FILE"
[[ -n "$SIGNATURE_FORMAT" ]] || die "signature metadata requires --signature-format"
case "$SIGNATURE_FORMAT" in
minisign|gpg|sigstore|external|other) ;;
*) die "unsupported --signature-format: $SIGNATURE_FORMAT" ;;
esac
[[ -n "$SIGNATURE_VERIFICATION_TOOL" ]] || die "signature metadata requires --signature-verification-tool"
[[ -n "$SIGNATURE_VERIFIED_SHA256" ]] || die "signature metadata requires --signature-verified-sha256"
[[ "$SIGNATURE_VERIFIED_SHA256" == "$SHA256_DIGEST" ]] || die "signature verified SHA-256 does not match artifact SHA-256"
if [[ -z "$SIGNATURE_KEY_FINGERPRINT" && -z "$SIGNATURE_CERTIFICATE_IDENTITY" ]]; then
die "signature metadata requires --signature-key-fingerprint or --signature-certificate-identity"
fi
SIGNATURE_METADATA_PROVIDED=true
SIGNATURE_VERIFICATION_PERFORMED=true
SIGNATURE_VERIFICATION_STATUS="verified"
SIGNATURE_FILE_SHA256="$(compute_sha256 "$SIGNATURE_FILE")"
}
if [[ "$BUILD_ARTIFACT" == true ]]; then
build_with_cargo
fi
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
fi
[[ -f "$ARTIFACT_PATH" ]] || die "artifact not found: $ARTIFACT_PATH"
KIND="$(artifact_kind "$ARTIFACT_PATH")"
[[ "$KIND" != "unknown" ]] || die "artifact kind is unsupported: $ARTIFACT_PATH"
PLATFORM_OUT_DIR="$OUT_DIR/$PLATFORM"
mkdir -p "$PLATFORM_OUT_DIR"
ARTIFACT_NAME="$(basename "$ARTIFACT_PATH")"
ARTIFACT_OUTPUT="$PLATFORM_OUT_DIR/$ARTIFACT_NAME"
if [[ "$(absolute_path "$ARTIFACT_PATH")" != "$(absolute_path "$ARTIFACT_OUTPUT")" ]]; then
cp -p "$ARTIFACT_PATH" "$ARTIFACT_OUTPUT"
fi
SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.txt"
RAW_SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.raw.txt"
NM_TOOL="$(select_nm_tool)"
info "capturing exported symbols with $NM_TOOL"
if ! "$NM_TOOL" -g --defined-only "$ARTIFACT_OUTPUT" > "$RAW_SYMBOLS_FILE" 2> "$PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"; then
die "symbol inventory failed; see $PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"
fi
awk '{print $NF}' "$RAW_SYMBOLS_FILE" \
| sed 's/^_//' \
| grep -E '^(litelib_[A-Za-z0-9_]*|blake3_PW)$' \
| sort -u > "$SYMBOLS_FILE" || true
[[ -s "$SYMBOLS_FILE" ]] || die "no SDXL C ABI symbols were found in $ARTIFACT_OUTPUT"
MISSING_SYMBOLS=()
for required in "${REQUIRED_SYMBOLS[@]}"; do
if ! grep -Fxq "$required" "$SYMBOLS_FILE"; then
MISSING_SYMBOLS+=("$required")
fi
done
if [[ ${#MISSING_SYMBOLS[@]} -ne 0 ]]; then
printf '%s\n' "${MISSING_SYMBOLS[@]}" > "$PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
die "artifact is missing required symbols; see $PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
fi
SHA256_DIGEST="$(compute_sha256 "$ARTIFACT_OUTPUT")"
validate_signature_metadata
ARTIFACT_SIZE_BYTES="$(wc -c < "$ARTIFACT_OUTPUT" | tr -d ' ')"
PROJECT_REVISION="$(source_revision_for "$PROJECT_ROOT")"
BACKEND_REVISION="$(source_revision_for "$BACKEND_SOURCE_DIR")"
BACKEND_DEPENDENCY_REVISION=""
if [[ -n "$BACKEND_DEPENDENCY_DIR" ]]; then
BACKEND_DEPENDENCY_REVISION="$(source_revision_for "$BACKEND_DEPENDENCY_DIR")"
fi
ARTIFACT_SET_ID="$PLATFORM-${SHA256_DIGEST:0:16}"
REPRODUCIBLE_MANIFEST_VALUE=false
if [[ "$BUILD_ARTIFACT" == true && "$REPRODUCIBLE" == true ]]; then
REPRODUCIBLE_MANIFEST_VALUE=true
fi
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=false
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=true
fi
FILE_DESCRIPTION="unknown"
if command -v file >/dev/null 2>&1; then
FILE_DESCRIPTION="$(file -b "$ARTIFACT_OUTPUT")"
fi
MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
{
printf '{\n'
printf ' "schema": "dragonx.lite.backend-artifact.v1",\n'
printf ' "generated_by": "scripts/build-lite-backend-artifact.sh",\n'
printf ' "read_only_inventory": true,\n'
printf ' "artifact_mutation_requested": false,\n'
printf ' "upload_requested": false,\n'
printf ' "signing_requested": false,\n'
printf ' "publication_requested": false,\n'
printf ' "signature_verification": {\n'
printf ' "policy_name": '; json_escape "$SIGNATURE_POLICY_NAME"; printf ',\n'
printf ' "policy_defined": %s,\n' "$SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE"
printf ' "required_for_release": %s,\n' "$SIGNATURE_REQUIRED_MANIFEST_VALUE"
printf ' "metadata_read_only": true,\n'
printf ' "metadata_provided": %s,\n' "$SIGNATURE_METADATA_PROVIDED"
printf ' "verification_performed": %s,\n' "$SIGNATURE_VERIFICATION_PERFORMED"
printf ' "verification_status": '; json_escape "$SIGNATURE_VERIFICATION_STATUS"; printf ',\n'
printf ' "signature_format": '; json_escape "$SIGNATURE_FORMAT"; printf ',\n'
printf ' "signature_path": '; json_escape "$SIGNATURE_FILE"; printf ',\n'
printf ' "signature_file_sha256": '; json_escape "$SIGNATURE_FILE_SHA256"; printf ',\n'
printf ' "verification_tool": '; json_escape "$SIGNATURE_VERIFICATION_TOOL"; printf ',\n'
printf ' "verification_command": '; json_escape "$SIGNATURE_VERIFICATION_COMMAND"; printf ',\n'
printf ' "key_fingerprint": '; json_escape "$SIGNATURE_KEY_FINGERPRINT"; printf ',\n'
printf ' "certificate_identity": '; json_escape "$SIGNATURE_CERTIFICATE_IDENTITY"; printf ',\n'
printf ' "certificate_issuer": '; json_escape "$SIGNATURE_CERTIFICATE_ISSUER"; printf ',\n'
printf ' "transparency_log_url": '; json_escape "$SIGNATURE_TRANSPARENCY_LOG_URL"; printf ',\n'
printf ' "verified_artifact_sha256": '; json_escape "$SIGNATURE_VERIFIED_SHA256"; printf '\n'
printf ' },\n'
printf ' "abi_version": '; json_escape "$ABI_VERSION"; printf ',\n'
printf ' "link_mode": '; json_escape "$LINK_MODE"; printf ',\n'
printf ' "platform": '; json_escape "$PLATFORM"; printf ',\n'
printf ' "rust_target": '; json_escape "$RUST_TARGET"; printf ',\n'
printf ' "artifact": {\n'
printf ' "path": '; json_escape "$ARTIFACT_OUTPUT"; printf ',\n'
printf ' "kind": '; json_escape "$KIND"; printf ',\n'
printf ' "size_bytes": %s,\n' "$ARTIFACT_SIZE_BYTES"
printf ' "sha256": '; json_escape "$SHA256_DIGEST"; printf ',\n'
printf ' "file_description": '; json_escape "$FILE_DESCRIPTION"; printf '\n'
printf ' },\n'
printf ' "symbol_inventory": {\n'
printf ' "tool": '; json_escape "$NM_TOOL"; printf ',\n'
printf ' "symbols_path": '; json_escape "$SYMBOLS_FILE"; printf ',\n'
printf ' "raw_symbols_path": '; json_escape "$RAW_SYMBOLS_FILE"; printf ',\n'
printf ' "required_symbols": '; json_array "${REQUIRED_SYMBOLS[@]}"; printf ',\n'
printf ' "exported_symbols": '; json_array_from_file "$SYMBOLS_FILE"; printf ',\n'
printf ' "missing_required_symbols": []\n'
printf ' },\n'
printf ' "provenance": {\n'
printf ' "owner_ready": true,\n'
printf ' "metadata_provided": true,\n'
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
printf ' "portable_dependency_override": %s,\n' "$PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE"
printf ' "silentdragonxlitelib_source": '; json_escape "$BACKEND_DEPENDENCY_DIR"; printf ',\n'
printf ' "builder": '; json_escape "$BUILDER"; printf ',\n'
printf ' "source_revision": '; json_escape "$BACKEND_REVISION"; printf ',\n'
printf ' "silentdragonxlitelib_revision": '; json_escape "$BACKEND_DEPENDENCY_REVISION"; printf ',\n'
printf ' "project_revision": '; json_escape "$PROJECT_REVISION"; printf ',\n'
printf ' "artifact_set_id": '; json_escape "$ARTIFACT_SET_ID"; printf ',\n'
printf ' "source_date_epoch": '; json_escape "$SOURCE_DATE_EPOCH_VALUE"; printf ',\n'
printf ' "reproducible": %s,\n' "$REPRODUCIBLE_MANIFEST_VALUE"
printf ' "redacted": true\n'
printf ' }\n'
printf '}\n'
} > "$MANIFEST_FILE"
info "artifact: $ARTIFACT_OUTPUT"
info "symbols: $SYMBOLS_FILE"
info "manifest: $MANIFEST_FILE"
info "sha256: $SHA256_DIGEST"
cat <<EOF
CMake configure example:
cmake -S "$PROJECT_ROOT" -B "$PROJECT_ROOT/build/lite" \\
-DDRAGONX_BUILD_LITE=ON \\
-DDRAGONX_ENABLE_LITE_BACKEND=ON \\
-DDRAGONX_LITE_BACKEND_LIBRARY="$ARTIFACT_OUTPUT" \\
-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE="$SYMBOLS_FILE" \\
-DDRAGONX_LITE_BACKEND_MANIFEST="$MANIFEST_FILE" \\
-DDRAGONX_LITE_BACKEND_LINK_MODE=$LINK_MODE \\
-DDRAGONX_LITE_BACKEND_ABI=$ABI_VERSION
EOF

View File

@@ -62,19 +62,8 @@ namespace {
bool isPageEnabledForBuild(ui::NavPage page)
{
#if DRAGONX_LITE_BUILD
switch (page) {
case ui::NavPage::Console:
case ui::NavPage::Peers:
case ui::NavPage::Explorer:
return false;
default:
return true;
}
#else
(void)page;
return true;
#endif
return wallet::isUiSurfaceAvailable(
wallet::currentWalletCapabilities(), ui::NavPageSurface(page));
}
std::string unencryptedTransactionHistoryCacheKey(const std::string& walletIdentity)
@@ -204,7 +193,7 @@ void App::tryConnect()
connection_status_ = TR("sb_no_conf");
// Try to start embedded daemon if enabled
if (use_embedded_daemon_ && !isEmbeddedDaemonRunning()) {
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
connection_status_ = TR("sb_starting_daemon");
if (startEmbeddedDaemon()) {
// Will retry connection after daemon starts
@@ -222,7 +211,7 @@ void App::tryConnect()
daemon_controller_ ? daemon_controller_->lastError().c_str() : "(no daemon object)",
daemon::EmbeddedDaemon::findDaemonBinary().c_str());
}
} else if (!use_embedded_daemon_) {
} else if (!isUsingEmbeddedDaemon()) {
VERBOSE_LOGF("[connect #%d] Embedded daemon disabled (using external). No config found at %s\n",
connect_attempt, confPath.c_str());
}
@@ -273,7 +262,7 @@ void App::tryConnect()
daemon_controller_->lastError().empty() ? "(none)" : daemon_controller_->lastError().c_str());
} else {
VERBOSE_LOGF("[connect #%d] No embedded daemon object (use_embedded=%s)\n",
attempt, use_embedded_daemon_ ? "yes" : "no");
attempt, isUsingEmbeddedDaemon() ? "yes" : "no");
}
worker_->post([this, config, daemonStarting, externalDetected, attempt]() -> rpc::RPCWorker::MainCb {
@@ -377,7 +366,7 @@ void App::tryConnect()
onDisconnected("Connection failed");
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
if (use_embedded_daemon_ && !isEmbeddedDaemonRunning()) {
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
// Prevent infinite crash-restart loop
if (daemon_controller_ && daemon_controller_->crashCount() >= 3) {
{ char buf[128]; snprintf(buf, sizeof(buf), TR("sb_daemon_crashed"), daemon_controller_->crashCount());
@@ -397,7 +386,7 @@ void App::tryConnect()
daemon_controller_ ? daemon_controller_->lastError().c_str() : "(no daemon object)");
}
}
} else if (!use_embedded_daemon_) {
} else if (!isUsingEmbeddedDaemon()) {
VERBOSE_LOGF("[connect #%d] Embedded daemon disabled — external daemon at %s:%s not responding\n",
attempt, config.host.c_str(), config.port.c_str());
} else {
@@ -1392,11 +1381,11 @@ void App::refreshMarketData()
void App::startMining(int threads)
{
#if DRAGONX_LITE_BUILD
(void)threads;
ui::Notifications::instance().warning("Solo mining is unavailable in lite build");
return;
#endif
if (!supportsSoloMining()) {
(void)threads;
ui::Notifications::instance().warning("Solo mining is unavailable in lite build");
return;
}
if (!state_.connected || !rpc_ || !worker_) return;
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
@@ -1426,9 +1415,7 @@ void App::startMining(int threads)
void App::stopMining()
{
#if DRAGONX_LITE_BUILD
return;
#endif
if (!supportsSoloMining()) return;
if (!state_.connected || !rpc_ || !worker_) return;
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
@@ -1454,6 +1441,12 @@ void App::stopMining()
void App::startPoolMining(int threads)
{
if (!supportsPoolMining()) {
(void)threads;
ui::Notifications::instance().warning("Pool mining is unavailable in this build");
return;
}
if (!xmrig_manager_)
xmrig_manager_ = std::make_unique<daemon::XmrigManager>();

View File

@@ -616,7 +616,7 @@ void App::checkIdleMining() {
if (idle_mining_active_) {
idle_mining_active_ = false;
idle_scaled_to_idle_ = false;
if (settings_ && settings_->getPoolMode()) {
if (settings_ && (settings_->getPoolMode() || !supportsSoloMining())) {
if (xmrig_manager_ && xmrig_manager_->isRunning())
stopPoolMining();
} else {
@@ -634,7 +634,8 @@ void App::checkIdleMining() {
int idleSec = util::Platform::getSystemIdleSeconds();
int delay = settings_->getMineIdleDelay();
bool isPool = settings_->getPoolMode();
bool isPool = settings_->getPoolMode() || !supportsSoloMining();
if (isPool && !supportsPoolMining()) return;
bool threadScaling = settings_->getIdleThreadScaling();
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());

View File

@@ -72,6 +72,11 @@ WizardUiState s_wizardUi;
void App::restartWizard()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Lite wallet lifecycle requests are available from Settings as dry-run readiness checks");
return;
}
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
// Reset crash counter for fresh wizard attempt
@@ -804,6 +809,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
@@ -812,6 +818,7 @@ void App::renderFirstRunWizard() {
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -985,6 +992,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
@@ -993,6 +1001,7 @@ void App::renderFirstRunWizard() {
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -1002,6 +1011,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
@@ -1010,6 +1020,7 @@ void App::renderFirstRunWizard() {
bootstrap_->start(dataDir, mirrorUrl);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
}

1855
src/chat/chat_protocol.cpp Normal file

File diff suppressed because it is too large Load Diff

586
src/chat/chat_protocol.h Normal file
View File

@@ -0,0 +1,586 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#ifndef DRAGONX_ENABLE_CHAT
#define DRAGONX_ENABLE_CHAT 0
#endif
namespace dragonx::chat {
enum class HushChatHeaderType {
Message,
ContactRequest
};
struct HushChatHeader {
int header_number = 0;
int version = 0;
std::string reply_zaddr;
std::string conversation_id;
HushChatHeaderType type = HushChatHeaderType::Message;
std::string secretstream_header_hex;
std::string public_key_hex;
};
struct HushChatHeaderParseResult {
bool ok = false;
HushChatHeader header;
std::string error;
};
struct HushChatMemoOutput {
std::size_t position = 0;
std::string memo;
};
struct HushChatMemoPair {
HushChatHeader header;
std::size_t header_position = 0;
std::size_t payload_position = 0;
std::string payload_memo;
};
enum class HushChatMemoGroupingIssue {
InvalidHeader,
MissingPayload,
DuplicateHeader,
OversizedMemo
};
struct HushChatMemoGroupingIssueInfo {
HushChatMemoGroupingIssue issue = HushChatMemoGroupingIssue::InvalidHeader;
std::size_t position = 0;
std::string detail;
};
struct HushChatMemoGroupingResult {
std::vector<HushChatMemoPair> pairs;
std::vector<HushChatMemoGroupingIssueInfo> issues;
std::size_t ignored_memo_count = 0;
};
struct HushChatTransactionInput {
std::string txid;
std::vector<HushChatMemoOutput> outputs;
};
struct HushChatTransactionMetadata {
std::string txid;
HushChatHeaderType type = HushChatHeaderType::Message;
std::string conversation_id;
std::string reply_zaddr;
std::size_t header_position = 0;
std::size_t payload_position = 0;
std::size_t payload_size = 0;
};
struct HushChatTransactionExtractionResult {
bool feature_enabled = false;
std::vector<HushChatTransactionMetadata> metadata;
std::vector<HushChatMemoGroupingIssueInfo> issues;
std::size_t ignored_memo_count = 0;
};
enum class HushChatDecryptPreflightError {
None,
FeatureDisabled,
NonMessageHeader,
InvalidHeaderNumber,
UnsupportedVersion,
MissingReplyAddress,
MissingConversationId,
InvalidSecretstreamHeader,
InvalidPublicKey,
EmptyCiphertext,
OversizedCiphertext,
OddLengthCiphertext,
InvalidCiphertextHex,
TruncatedCiphertext
};
struct HushChatDecryptPreflightInput {
HushChatHeader header;
std::string ciphertext_hex;
};
struct HushChatDecryptPreflightResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptPreflightError error = HushChatDecryptPreflightError::None;
const char* error_name = "None";
std::size_t ciphertext_size = 0;
};
enum class HushChatHexDecodeError {
None,
Empty,
OddLength,
InvalidHex,
UnexpectedByteLength
};
struct HushChatHexDecodeResult {
bool ok = false;
HushChatHexDecodeError error = HushChatHexDecodeError::None;
const char* error_name = "None";
std::vector<unsigned char> bytes;
};
enum class HushChatDecryptDirection {
Incoming,
Outgoing
};
enum class HushChatSessionKeySelection {
ClientRx,
ServerTx
};
enum class HushChatDecryptInputError {
None,
FeatureDisabled,
InvalidStoredChatKey,
DecryptPreflightFailed,
InvalidPeerPublicKey,
InvalidStreamHeader,
InvalidCiphertext
};
struct HushChatDecryptInputMaterial {
std::string stored_chat_key_hex;
HushChatHeader header;
std::string ciphertext_hex;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
std::string peer_public_key_hex;
};
struct HushChatPreparedDecryptInput {
std::vector<unsigned char> stored_chat_key_bytes;
std::vector<unsigned char> seed_bytes;
std::vector<unsigned char> peer_public_key_bytes;
std::vector<unsigned char> stream_header_bytes;
std::vector<unsigned char> ciphertext_bytes;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t plaintext_capacity = 0;
};
struct HushChatDecryptInputPreparationResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptInputError error = HushChatDecryptInputError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatPreparedDecryptInput prepared;
};
struct HushChatDecryptFixtureReadinessResult {
bool ready = false;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t stream_header_size = 0;
std::size_t ciphertext_size = 0;
std::size_t plaintext_capacity = 0;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
};
enum class HushChatCompatibilityFixtureError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidLocalPublicKey,
InvalidPeerPublicKey,
InvalidHeaderMemo,
InvalidMemoPair,
NonMemoHeader,
HeaderPublicKeyMismatch,
DecryptInputFailed,
NotFixtureReady,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
ExpectedPeerPublicKeyLengthMismatch,
ExpectedStreamHeaderLengthMismatch,
ExpectedCiphertextLengthMismatch,
ExpectedPlaintextLengthMismatch,
ExpectedRoleMismatch,
InvalidPlaintextHash
};
struct HushChatCompatibilityFixture {
std::string fixture_id;
std::string stored_chat_key_hex;
std::string local_public_key_hex;
std::string peer_public_key_hex;
std::string header_memo;
std::string ciphertext_memo;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection expected_session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t expected_stored_chat_key_size = 32;
std::size_t expected_seed_size = 32;
std::size_t expected_local_public_key_size = 32;
std::size_t expected_peer_public_key_size = 32;
std::size_t expected_stream_header_size = 24;
std::size_t expected_ciphertext_size = 0;
std::size_t expected_plaintext_size = 0;
std::string expected_plaintext_hash_hex;
};
struct HushChatCompatibilityFixtureVerificationResult {
bool ok = false;
bool feature_enabled = false;
HushChatCompatibilityFixtureError error = HushChatCompatibilityFixtureError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptInputError decrypt_input_error = HushChatDecryptInputError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatHeader header;
HushChatDecryptInputPreparationResult preparation;
HushChatDecryptFixtureReadinessResult readiness;
std::size_t local_public_key_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t plaintext_hash_size = 0;
};
enum class HushChatCompatibilityFixtureKind {
IncomingMemo,
OutgoingMemo,
SeedPublicKeyProjection,
CorruptedAuthFailure,
ContactExclusion
};
enum class HushChatCompatibilityFixtureFileStatus {
Pending,
Ready
};
enum class HushChatCompatibilityFixtureFileError {
None,
FeatureDisabled,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingKind,
UnknownKind,
MissingStatus,
UnknownStatus,
MissingFixtureId,
MissingPendingReason,
MissingFixtureObject,
InvalidFixtureField,
FixtureVerificationFailed,
ContactFixtureNotExcluded,
FileReadFailed
};
struct HushChatCompatibilityFixtureFile {
std::string schema;
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureFileStatus status = HushChatCompatibilityFixtureFileStatus::Pending;
std::string fixture_id;
std::string pending_reason;
HushChatCompatibilityFixture fixture;
};
struct HushChatCompatibilityFixtureFileParseResult {
bool ok = false;
bool feature_enabled = false;
bool pending = false;
bool verified = false;
bool excluded_from_decrypt = false;
HushChatCompatibilityFixtureFileError error = HushChatCompatibilityFixtureFileError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFile file;
HushChatCompatibilityFixtureVerificationResult verification;
};
enum class HushChatSeedPublicKeyProjectionError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidStoredChatKey,
InvalidLocalPublicKey,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
SodiumInitializationFailed,
KeypairProjectionFailed,
ProjectedPublicKeyMismatch
};
struct HushChatSeedPublicKeyProjectionResult {
bool ok = false;
bool feature_enabled = false;
HushChatSeedPublicKeyProjectionError error = HushChatSeedPublicKeyProjectionError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t local_public_key_size = 0;
std::size_t projected_public_key_size = 0;
};
enum class HushChatCorruptedAuthFailureReadinessError {
None,
FeatureDisabled,
FixturePending,
WrongFixtureKind,
FixtureNotVerified,
SeedProjectionNotVerified
};
struct HushChatCorruptedAuthFailureReadinessResult {
bool ok = false;
bool feature_enabled = false;
bool structurally_ready_for_future_auth_check = false;
bool requires_future_secretstream_auth_failure = false;
bool decrypted = false;
bool authenticated = false;
HushChatCorruptedAuthFailureReadinessError error = HushChatCorruptedAuthFailureReadinessError::None;
const char* error_name = "None";
};
enum class HushChatCompatibilityFixtureImportError {
None,
FeatureDisabled,
MissingRequiredKind,
DuplicateKind,
FixtureLoadFailed,
FixtureKindMismatch,
FixturePending,
FixtureInvalid,
FixtureNotVerified,
SeedProjectionFailed,
AuthFailureScaffoldFailed,
ContactFixtureNotExcluded
};
struct HushChatCompatibilityFixtureImportCandidate {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
};
struct HushChatCompatibilityFixtureImportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFileParseResult parsed;
HushChatSeedPublicKeyProjectionResult seed_projection;
HushChatCorruptedAuthFailureReadinessResult auth_failure_readiness;
};
struct HushChatCompatibilityFixtureImportChecklistResult {
bool ok = false;
bool feature_enabled = false;
bool replacement_ready = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureImportItem> items;
};
struct HushChatCompatibilityFixtureReplacementReportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool refused = true;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
bool cont_excluded = false;
bool decrypted = false;
bool authenticated = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
};
struct HushChatCompatibilityFixtureReplacementDryRunResult {
bool ok = false;
bool feature_enabled = false;
bool dry_run_only = true;
bool redacted_report = true;
bool would_replace = false;
bool replacement_refused = true;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureReplacementReportItem> report_items;
};
enum class HushChatCaptureManifestError {
None,
FeatureDisabled,
FileReadFailed,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingManifestId,
MissingStatus,
UnknownStatus,
MissingFixtureDirectory,
MissingDryRunCommand,
InvalidDryRunCommand,
MissingProvenance,
MissingSourceClient,
InvalidSourceClient,
MissingSourceClientVersion,
MissingCaptureDate,
MissingNetwork,
MissingCaptureMethod,
MissingHandling,
MissingHandlingFlag,
HandlingFlagNotTrue,
MissingCategories,
InvalidCategoryEntry,
UnknownCategory,
DuplicateCategory,
MissingRequiredCategory,
ProhibitedFieldPresent
};
enum class HushChatCaptureManifestStatus {
Staged
};
struct HushChatCaptureManifestCategoryReport {
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string staged_filename;
bool declared = false;
};
struct HushChatCaptureManifestValidationResult {
bool ok = false;
bool feature_enabled = false;
bool redacted_report = true;
bool validates_provenance_only = true;
bool no_sensitive_material_declared = false;
bool has_dry_run_command = false;
HushChatCaptureManifestError error = HushChatCaptureManifestError::None;
const char* error_name = "None";
HushChatCaptureManifestStatus status = HushChatCaptureManifestStatus::Staged;
std::string manifest_path;
std::string fixture_directory;
std::size_t required_count = 0;
std::size_t declared_count = 0;
std::size_t missing_count = 0;
std::size_t duplicate_count = 0;
std::size_t prohibited_field_count = 0;
std::size_t handling_flag_count = 0;
std::vector<HushChatCaptureManifestCategoryReport> categories;
};
constexpr int kHushChatSupportedVersion = 0;
constexpr std::size_t kHushChatMemoByteLimit = 512;
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
constexpr std::size_t kHushChatSecretstreamABytes = 17;
constexpr std::size_t kHushChatStoredChatKeyByteLength = 32;
constexpr std::size_t kHushChatStoredChatKeyHexLength = kHushChatStoredChatKeyByteLength * 2;
constexpr std::size_t kHushChatSeedByteLength = 32;
constexpr std::size_t kHushChatPublicKeyByteLength = kHushChatPublicKeyHexLength / 2;
constexpr std::size_t kHushChatSecretstreamHeaderByteLength = kHushChatSecretstreamHeaderHexLength / 2;
constexpr const char* kHushChatCompatibilityFixtureSchema = "dragonx.hushchat.compat-fixture.v1";
constexpr const char* kHushChatCaptureManifestSchema = "dragonx.hushchat.capture-manifest.v1";
constexpr bool hushChatFeatureEnabledAtBuild()
{
return DRAGONX_ENABLE_CHAT != 0;
}
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo);
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs);
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
const HushChatTransactionInput& transaction,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatDecryptPreflightResult validateHushChatMemoDecryptPreflight(
const HushChatDecryptPreflightInput& input,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatHexDecodeResult decodeHushChatHexBytes(const std::string& hex,
std::size_t expectedByteLength);
HushChatDecryptInputPreparationResult prepareHushChatDecryptInput(
const HushChatDecryptInputMaterial& material,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatDecryptFixtureReadinessResult inspectHushChatDecryptFixtureReadiness(
const HushChatPreparedDecryptInput& prepared);
HushChatSessionKeySelection hushChatSessionKeySelectionForDirection(HushChatDecryptDirection direction);
HushChatCompatibilityFixtureVerificationResult verifyHushChatCompatibilityFixture(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult parseHushChatCompatibilityFixtureFile(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult loadHushChatCompatibilityFixtureFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatSeedPublicKeyProjectionResult verifyHushChatSeedPublicKeyProjection(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCorruptedAuthFailureReadinessResult inspectHushChatCorruptedAuthFailureReadiness(
const HushChatCompatibilityFixtureFileParseResult& parsed,
const HushChatSeedPublicKeyProjectionResult& seedProjection,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
std::vector<HushChatCompatibilityFixtureKind> hushChatRequiredCompatibilityFixtureKinds();
HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureReplacementDryRunResult inspectHushChatCompatibilityFixtureReplacementDryRun(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult validateHushChatCaptureManifest(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult loadHushChatCaptureManifestFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
const char* hushChatHeaderTypeName(HushChatHeaderType type);
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue);
const char* hushChatDecryptPreflightErrorName(HushChatDecryptPreflightError error);
const char* hushChatHexDecodeErrorName(HushChatHexDecodeError error);
const char* hushChatDecryptDirectionName(HushChatDecryptDirection direction);
const char* hushChatSessionKeySelectionName(HushChatSessionKeySelection selection);
const char* hushChatDecryptInputErrorName(HushChatDecryptInputError error);
const char* hushChatCompatibilityFixtureErrorName(HushChatCompatibilityFixtureError error);
const char* hushChatCompatibilityFixtureKindName(HushChatCompatibilityFixtureKind kind);
const char* hushChatCompatibilityFixtureFileStatusName(HushChatCompatibilityFixtureFileStatus status);
const char* hushChatCompatibilityFixtureFileErrorName(HushChatCompatibilityFixtureFileError error);
const char* hushChatSeedPublicKeyProjectionErrorName(HushChatSeedPublicKeyProjectionError error);
const char* hushChatCorruptedAuthFailureReadinessErrorName(HushChatCorruptedAuthFailureReadinessError error);
const char* hushChatCompatibilityFixtureImportErrorName(HushChatCompatibilityFixtureImportError error);
const char* hushChatCaptureManifestErrorName(HushChatCaptureManifestError error);
} // namespace dragonx::chat

View File

@@ -1,31 +1,3 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
// !! 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.2.0-rc1"
#define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 2
#define DRAGONX_VERSION_PATCH 0
#define DRAGONX_APP_NAME "ObsidianDragonLite"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
#define DRAGONX_DEFAULT_RPC_PORT "21769"
// Coin parameters
#define DRAGONX_TICKER "DRGX"
#define DRAGONX_COIN_NAME "DragonX"
#define DRAGONX_URI_SCHEME "drgx"
#define DRAGONX_ZATOSHI_PER_COIN 100000000
#define DRAGONX_DEFAULT_FEE 0.0001
// Config file names
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
#define DRAGONX_WALLET_FILENAME "wallet.dat"
#include "dragonx_generated_version.h"

View File

@@ -4,7 +4,7 @@
#pragma once
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
// !! DO NOT EDIT generated version output — it is generated from version.h.in by CMake.
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
#define DRAGONX_VERSION "@PROJECT_VERSION@@DRAGONX_VERSION_SUFFIX@"

View File

@@ -6,6 +6,7 @@
#include <cstdlib>
#include <map>
#include <set>
#include <unordered_map>
using json = nlohmann::json;
@@ -136,6 +137,60 @@ void appendMissingPreviousTransactions(std::vector<TransactionInfo>& transaction
}
}
using HushChatMemoOutputMap = std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>;
std::size_t readMemoPosition(const json& source, std::size_t fallback)
{
for (const char* key : {"position", "outputIndex", "outindex"}) {
auto value = readOptional<int>(source, key);
if (value && *value >= 0) return static_cast<std::size_t>(*value);
}
return fallback;
}
void recordHushChatMemoOutput(HushChatMemoOutputMap* outputs,
const std::string& txid,
const std::string& memo,
std::size_t position)
{
if (!outputs || txid.empty() || memo.empty()) return;
(*outputs)[txid].push_back(chat::HushChatMemoOutput{position, memo});
}
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
const std::string& txid,
const std::vector<chat::HushChatMemoOutput>& outputs)
{
auto extracted = chat::extractHushChatTransactionMetadata(chat::HushChatTransactionInput{txid, outputs});
for (auto& metadata : extracted.metadata) {
destination.push_back(std::move(metadata));
}
}
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
const std::string& txid,
const NetworkRefreshService::TransactionViewCacheEntry& entry)
{
if (!chat::hushChatFeatureEnabledAtBuild()) return;
std::vector<chat::HushChatMemoOutput> outputs;
outputs.reserve(entry.outgoing_outputs.size());
for (const auto& output : entry.outgoing_outputs) {
if (!output.memo.empty()) outputs.push_back(chat::HushChatMemoOutput{output.position, output.memo});
}
appendExtractedHushChatMetadata(destination, txid, outputs);
}
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
const HushChatMemoOutputMap& outputsByTxid)
{
if (!chat::hushChatFeatureEnabledAtBuild()) return;
for (const auto& [txid, outputs] : outputsByTxid) {
appendExtractedHushChatMetadata(destination, txid, outputs);
}
}
} // namespace
NetworkRefreshService::ConnectionInfoResult NetworkRefreshService::parseConnectionInfoResult(const json& info)
@@ -614,17 +669,23 @@ void NetworkRefreshService::appendShieldedReceivedTransactions(std::vector<Trans
std::set<std::string>& knownTxids,
const std::string& address,
const json& received,
const std::set<std::string>& miningAddresses)
const std::set<std::string>& miningAddresses,
HushChatMemoOutputMap* chatMemoOutputs)
{
if (received.is_null() || !received.is_array()) return;
std::size_t fallbackPosition = 0;
for (const auto& note : received) {
const std::size_t notePosition = readMemoPosition(note, fallbackPosition++);
auto txid = readOptional<std::string>(note, "txid");
if (!txid || txid->empty()) continue;
auto change = readOptional<bool>(note, "change");
if (change && *change) continue;
auto memoValue = readOptional<std::string>(note, "memoStr");
if (memoValue) recordHushChatMemoOutput(chatMemoOutputs, *txid, *memoValue, notePosition);
bool dominated = false;
for (const auto& existing : transactions) {
if (existing.txid == *txid && existing.type == "receive") {
@@ -641,7 +702,7 @@ void NetworkRefreshService::appendShieldedReceivedTransactions(std::vector<Trans
if (auto value = readOptional<double>(note, "amount")) info.amount = *value;
if (auto value = readOptional<int>(note, "confirmations")) info.confirmations = *value;
if (auto value = readOptional<std::int64_t>(note, "time")) info.timestamp = *value;
if (auto value = readOptional<std::string>(note, "memoStr")) info.memo = *value;
if (memoValue) info.memo = *memoValue;
knownTxids.insert(*txid);
transactions.push_back(std::move(info));
}
@@ -663,7 +724,9 @@ NetworkRefreshService::TransactionViewCacheEntry NetworkRefreshService::parseVie
}
if (viewTransaction.contains("outputs") && viewTransaction["outputs"].is_array()) {
std::size_t fallbackPosition = 0;
for (const auto& output : viewTransaction["outputs"]) {
const std::size_t outputPosition = readMemoPosition(output, fallbackPosition++);
bool outgoing = false;
if (auto value = readOptional<bool>(output, "outgoing")) outgoing = *value;
if (!outgoing) continue;
@@ -672,6 +735,7 @@ NetworkRefreshService::TransactionViewCacheEntry NetworkRefreshService::parseVie
if (auto value = readOptional<std::string>(output, "address")) out.address = *value;
if (auto value = readOptional<double>(output, "value")) out.value = *value;
if (auto value = readOptional<std::string>(output, "memoStr")) out.memo = *value;
out.position = outputPosition;
entry.outgoing_outputs.push_back(std::move(out));
}
}
@@ -738,6 +802,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
result.shieldedScanHeights = snapshot.shieldedScanHeights;
std::set<std::string> knownTxids;
HushChatMemoOutputMap hushChatReceivedOutputs;
HushChatMemoOutputMap* hushChatReceivedOutputsPtr = chat::hushChatFeatureEnabledAtBuild()
? &hushChatReceivedOutputs
: nullptr;
bool transactionRpcError = false;
try {
@@ -779,7 +847,12 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
nextSearchIndex = index + 1;
try {
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
appendShieldedReceivedTransactions(result.transactions, knownTxids, address, received, snapshot.miningAddresses);
appendShieldedReceivedTransactions(result.transactions,
knownTxids,
address,
received,
snapshot.miningAddresses,
hushChatReceivedOutputsPtr);
if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight;
} catch (const std::exception& e) {
transactionRpcError = true;
@@ -817,6 +890,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
if (cached != snapshot.viewTxCache.end()) {
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
appendViewTransactionOutputs(result.transactions, txid, cached->second);
appendExtractedHushChatMetadata(result.hushChatMetadata, txid, cached->second);
continue;
}
}
@@ -841,6 +915,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
auto entry = parseViewTransactionCacheEntry(viewTransaction);
appendViewTransactionOutputs(result.transactions, txid, entry);
appendExtractedHushChatMetadata(result.hushChatMetadata, txid, entry);
json rawTransaction;
bool hasRawTransaction = false;
@@ -885,6 +960,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
}
}
appendExtractedHushChatMetadata(result.hushChatMetadata, hushChatReceivedOutputs);
if (!snapshot.previousTransactions.empty()) {
if (transactionRpcError) {
appendMissingPreviousTransactions(result.transactions, snapshot.previousTransactions, true, snapshot.pendingOpids);
@@ -909,6 +986,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
result.transactions = snapshot.previousTransactions;
result.shieldedAddressCount = snapshot.shieldedAddresses.size();
result.shieldedScanHeights = snapshot.shieldedScanHeights;
HushChatMemoOutputMap hushChatReceivedOutputs;
HushChatMemoOutputMap* hushChatReceivedOutputsPtr = chat::hushChatFeatureEnabledAtBuild()
? &hushChatReceivedOutputs
: nullptr;
try {
std::set<std::string> recentTxids;
@@ -944,7 +1025,12 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
std::vector<TransactionInfo> scannedTransactions;
std::set<std::string> scannedTxids;
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
appendShieldedReceivedTransactions(scannedTransactions, scannedTxids, address, received, snapshot.miningAddresses);
appendShieldedReceivedTransactions(scannedTransactions,
scannedTxids,
address,
received,
snapshot.miningAddresses,
hushChatReceivedOutputsPtr);
for (auto& scanned : scannedTransactions) {
bool replaced = false;
for (auto& existing : result.transactions) {
@@ -967,6 +1053,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
(shieldedStart + shieldedLimit >= snapshot.shieldedAddresses.size()) ? 0 : shieldedStart + shieldedLimit;
result.shieldedScanComplete = true;
appendExtractedHushChatMetadata(result.hushChatMetadata, hushChatReceivedOutputs);
sortTransactionsNewestFirst(result.transactions);
return result;
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include "chat/chat_protocol.h"
#include "data/wallet_state.h"
#include "refresh_scheduler.h"
#include "rpc/rpc_worker.h"
@@ -159,6 +160,7 @@ public:
std::string address;
double value = 0.0;
std::string memo;
std::size_t position = 0;
};
std::vector<Output> outgoing_outputs;
};
@@ -180,6 +182,7 @@ public:
struct TransactionRefreshResult {
std::vector<TransactionInfo> transactions;
std::vector<chat::HushChatTransactionMetadata> hushChatMetadata;
int blockHeight = -1;
TransactionViewCache newViewTxEntries;
std::size_t nextShieldedScanStartIndex = 0;
@@ -260,7 +263,8 @@ public:
std::set<std::string>& knownTxids,
const std::string& address,
const nlohmann::json& received,
const std::set<std::string>& miningAddresses = {});
const std::set<std::string>& miningAddresses = {},
std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>* chatMemoOutputs = nullptr);
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
const std::string& txid,

View File

@@ -12,6 +12,7 @@
#include "schema/ui_schema.h"
#include "../embedded/IconsMaterialDesign.h"
#include "../util/i18n.h"
#include "../wallet/wallet_capabilities.h"
#include <cstdio>
#include <cmath>
@@ -66,14 +67,26 @@ inline const char* NavSectionLabel(const NavItem& item) {
return item.section_tr_key ? TR(item.section_tr_key) : item.section_label;
}
inline wallet::WalletUiSurface NavPageSurface(NavPage page)
{
switch (page) {
case NavPage::Overview: return wallet::WalletUiSurface::Overview;
case NavPage::Send: return wallet::WalletUiSurface::Send;
case NavPage::Receive: return wallet::WalletUiSurface::Receive;
case NavPage::History: return wallet::WalletUiSurface::History;
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
case NavPage::Market: return wallet::WalletUiSurface::Market;
case NavPage::Console: return wallet::WalletUiSurface::Console;
case NavPage::Peers: return wallet::WalletUiSurface::Peers;
case NavPage::Explorer: return wallet::WalletUiSurface::Explorer;
case NavPage::Settings: return wallet::WalletUiSurface::Settings;
default: return wallet::WalletUiSurface::Overview;
}
}
inline bool IsNavPageVisible(NavPage page)
{
#if DRAGONX_LITE_BUILD
return page != NavPage::Console && page != NavPage::Peers && page != NavPage::Explorer;
#else
(void)page;
return true;
#endif
return wallet::isUiSurfaceAvailable(wallet::currentWalletCapabilities(), NavPageSurface(page));
}
// Get the Material Design icon string for a navigation page.

View File

@@ -29,6 +29,7 @@ namespace ui {
class BootstrapDownloadDialog {
public:
static void show(App* app) {
if (!app || !app->supportsFullNodeLifecycleActions()) return;
s_open = true;
s_app = app;
s_state = State::Confirm;
@@ -285,6 +286,7 @@ private:
// ---- Shared: kick off download ----
static void startDownload(const std::string& url) {
if (!s_app || !s_app->supportsFullNodeLifecycleActions()) return;
s_wasDaemonRunning = s_app->stopDaemonForBootstrap();
s_app->setBootstrapDownloading(true);
s_bootstrap = std::make_unique<util::Bootstrap>();

View File

@@ -151,13 +151,11 @@ static void RenderMiningTabContent(App* app)
strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1);
s_pool_state_loaded = true;
}
#if DRAGONX_LITE_BUILD
if (!s_pool_mode) {
if (!app->supportsSoloMining() && !s_pool_mode) {
s_pool_mode = true;
app->settings()->setPoolMode(true);
app->settings()->save();
}
#endif
// Default pool worker to user's first shielded (z) address once available.
// For new wallets without a z-address, leave the field blank so the user
@@ -242,11 +240,11 @@ static void RenderMiningTabContent(App* app)
// MODE TOGGLE — SOLO | POOL segmented control
// ================================================================
{
const bool liteBuild = app->isLiteBuild();
const bool soloMiningAvailable = app->supportsSoloMining();
float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs;
float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size;
float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size;
float totalW = liteBuild ? toggleW : (toggleW * 2);
float totalW = soloMiningAvailable ? (toggleW * 2) : toggleW;
ImVec2 tMin = ImGui::GetCursorScreenPos();
ImVec2 tMax(tMin.x + totalW, tMin.y + toggleH);
@@ -258,7 +256,7 @@ static void RenderMiningTabContent(App* app)
ImVec2 soloMin = tMin;
ImVec2 soloMax(tMin.x + toggleW, tMax.y);
bool soloHov = material::IsRectHovered(soloMin, soloMax);
if (!liteBuild) {
if (soloMiningAvailable) {
// SOLO button (left half)
if (!s_pool_mode) {
dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd);
@@ -275,7 +273,7 @@ static void RenderMiningTabContent(App* app)
// POOL button (right half) — disabled when solo mining is active
bool soloMiningActive = mining.generate;
ImVec2 poolMin(tMin.x + (liteBuild ? 0.0f : toggleW), tMin.y);
ImVec2 poolMin(tMin.x + (soloMiningAvailable ? toggleW : 0.0f), tMin.y);
ImVec2 poolMax = tMax;
bool poolHov = material::IsRectHovered(poolMin, poolMax);
if (s_pool_mode) {
@@ -296,7 +294,7 @@ static void RenderMiningTabContent(App* app)
}
// Invisible buttons for click targets
if (!liteBuild) {
if (soloMiningAvailable) {
ImGui::SetCursorScreenPos(soloMin);
ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH));
if (ImGui::IsItemClicked() && s_pool_mode) {
@@ -310,7 +308,7 @@ static void RenderMiningTabContent(App* app)
ImGui::SetCursorScreenPos(poolMin);
ImGui::InvisibleButton("##PoolMode", ImVec2(toggleW, toggleH));
if (!liteBuild && ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) {
if (soloMiningAvailable && ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) {
s_pool_mode = true;
app->settings()->setPoolMode(true);
app->settings()->save();
@@ -318,7 +316,7 @@ static void RenderMiningTabContent(App* app)
// so no need to call stopMining() — it would just set the
// toggle-in-progress flag and make the button show "STARTING".
}
if (!liteBuild && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (soloMiningAvailable && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (poolHov && soloMiningActive && !s_pool_mode) {
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
}
@@ -1935,7 +1933,7 @@ static void RenderMiningTabContent(App* app)
&& tx.memo.find("Mining Pool payout") != std::string::npos);
if (isSoloMined || isPoolPayout) {
// Apply earnings filter
if (app->isLiteBuild()) {
if (!app->supportsSoloMining()) {
if (s_earnings_filter == 1 && !isPoolPayout) continue;
} else {
if (s_earnings_filter == 1 && !isSoloMined) continue;
@@ -1984,10 +1982,10 @@ static void RenderMiningTabContent(App* app)
// === Earnings filter toggle button (top-right of card) ===
{
const bool liteBuild = app->isLiteBuild();
const char* filterLabels[] = { TR("mining_filter_all"), liteBuild ? TR("mining_pool") : TR("mining_solo"), TR("mining_pool") };
const char* filterIcons[] = { ICON_MD_FUNCTIONS, liteBuild ? ICON_MD_CLOUD : ICON_MD_MEMORY, ICON_MD_CLOUD };
const int filterCount = liteBuild ? 2 : 3;
const bool soloMiningAvailable = app->supportsSoloMining();
const char* filterLabels[] = { TR("mining_filter_all"), soloMiningAvailable ? TR("mining_solo") : TR("mining_pool"), TR("mining_pool") };
const char* filterIcons[] = { ICON_MD_FUNCTIONS, soloMiningAvailable ? ICON_MD_MEMORY : ICON_MD_CLOUD, ICON_MD_CLOUD };
const int filterCount = soloMiningAvailable ? 3 : 2;
const char* curIcon = filterIcons[s_earnings_filter];
const char* curLabel = filterLabels[s_earnings_filter];
@@ -2033,7 +2031,7 @@ static void RenderMiningTabContent(App* app)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
const char* tips[] = {
TR("mining_filter_tip_all"),
liteBuild ? TR("mining_filter_tip_pool") : TR("mining_filter_tip_solo"),
soloMiningAvailable ? TR("mining_filter_tip_solo") : TR("mining_filter_tip_pool"),
TR("mining_filter_tip_pool")
};
ImGui::SetTooltip("%s", tips[s_earnings_filter]);

View File

@@ -0,0 +1,419 @@
#include "wallet/lite_backend_artifact_contract.h"
#include <algorithm>
#include <cctype>
#include <sstream>
#include <utility>
namespace dragonx::wallet {
namespace {
constexpr const char* kSupportedAbiVersion = "sdxl-c-v1";
using Issue = LiteBackendArtifactContractIssue;
using Status = LiteBackendArtifactContractStatus;
void addIssue(LiteBackendArtifactContractResult& result, Issue issue, std::string message)
{
result.issues.push_back({issue, std::move(message)});
if (result.error.empty()) result.error = result.issues.back().message;
}
LiteBackendArtifactContractResult stoppedResult(
LiteBackendArtifactContractResult result,
Status status,
Issue issue,
std::string message)
{
result.status = status;
addIssue(result, issue, std::move(message));
return result;
}
std::string trimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
bool containsSymbol(const std::vector<std::string>& exportedSymbols, const char* abiName)
{
return std::find(exportedSymbols.begin(), exportedSymbols.end(), abiName) != exportedSymbols.end();
}
bool importedArtifactKindLinkable(LiteBackendArtifactKind kind)
{
return kind == LiteBackendArtifactKind::StaticLibrary || kind == LiteBackendArtifactKind::SharedLibrary;
}
bool signatureTrustMetadataProvided(const LiteBackendArtifactSignatureVerificationMetadata& signature)
{
return !trimCopy(signature.keyFingerprint).empty() || !trimCopy(signature.certificateIdentity).empty();
}
LiteBackendArtifactContractResult validateSignatureMetadata(
LiteBackendArtifactContractResult result,
const LiteBackendArtifactContractInput& input,
LiteBackendArtifactContractOptions options)
{
const auto& signature = input.signatureVerification;
result.signatureRequiredForRelease = signature.requiredForRelease;
const bool shouldValidate = signature.requiredForRelease ||
(options.validateOptionalSignatureMetadata && signature.metadataProvided);
if (!shouldValidate) {
result.signatureMetadataAccepted = true;
result.signatureVerificationForwarded = signature.metadataProvided;
result.signatureVerified = signature.verified;
return result;
}
if (options.requireSignatureMetadataWhenRequired && !signature.policyDefined) {
return stoppedResult(std::move(result), Status::WaitingForSignatureMetadata, Issue::SignaturePolicyMissing,
"lite backend artifact signature policy is not defined");
}
if (!signature.metadataProvided) {
return stoppedResult(std::move(result), Status::WaitingForSignatureMetadata, Issue::SignatureMetadataMissing,
"lite backend artifact signature metadata is required but missing");
}
if (trimCopy(signature.signatureFormat).empty() ||
trimCopy(signature.signaturePath).empty() ||
trimCopy(signature.verificationTool).empty() ||
trimCopy(signature.verifiedArtifactSha256).empty()) {
return stoppedResult(std::move(result), Status::WaitingForSignatureMetadata, Issue::SignatureMetadataIncomplete,
"lite backend artifact signature metadata is incomplete");
}
if (!signatureTrustMetadataProvided(signature)) {
return stoppedResult(std::move(result), Status::WaitingForSignatureMetadata, Issue::SignatureTrustMetadataMissing,
"lite backend artifact signature metadata is missing a reviewed trust identity");
}
if (!signature.verificationPerformed) {
return stoppedResult(std::move(result), Status::WaitingForSignatureMetadata, Issue::SignatureVerificationMissing,
"lite backend artifact signature verification has not been performed");
}
if (!signature.verified) {
return stoppedResult(std::move(result), Status::WaitingForSignatureMetadata, Issue::SignatureVerificationFailed,
"lite backend artifact signature verification did not pass");
}
if (!trimCopy(input.artifactSha256).empty() && signature.verifiedArtifactSha256 != input.artifactSha256) {
return stoppedResult(std::move(result), Status::WaitingForSignatureMetadata, Issue::SignatureVerifiedArtifactShaMismatch,
"lite backend artifact signature metadata does not match the artifact SHA-256");
}
result.signatureMetadataAccepted = true;
result.signatureVerificationForwarded = true;
result.signatureVerified = true;
return result;
}
LiteBackendArtifactContractResult rejectRuntimeActions(
LiteBackendArtifactContractResult result,
const LiteBackendArtifactContractInput& input,
LiteBackendArtifactContractOptions options)
{
if (!options.rejectRuntimeActions) return result;
if (input.artifactMutationRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::ArtifactMutationRequested,
"lite backend artifact contract cannot mutate artifacts");
}
if (input.dynamicLibraryLoadRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::DynamicLibraryLoadRequested,
"lite backend artifact contract cannot load dynamic libraries");
}
if (input.dynamicLibraryUnloadRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::DynamicLibraryUnloadRequested,
"lite backend artifact contract cannot unload dynamic libraries");
}
if (input.symbolResolutionRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::SymbolResolutionRequested,
"lite backend artifact contract cannot resolve runtime symbols");
}
if (input.sdxlApiRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::SdxlApiRequested,
"lite backend artifact contract cannot call SDXL APIs");
}
if (input.bridgeCallRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::BridgeCallRequested,
"lite backend artifact contract cannot call the bridge");
}
if (input.serverConnectivityCheckRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::ServerConnectivityCheckRequested,
"lite backend artifact contract cannot check server connectivity");
}
if (input.walletLifecycleRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::WalletLifecycleRequested,
"lite backend artifact contract cannot execute wallet lifecycle flows");
}
if (input.syncRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::SyncRequested,
"lite backend artifact contract cannot start sync");
}
if (input.syncStatusPollingRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::SyncStatusPollingRequested,
"lite backend artifact contract cannot poll syncstatus");
}
if (input.workerQueueRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::WorkerQueueRequested,
"lite backend artifact contract cannot enqueue workers");
}
if (input.walletStateMutationRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::WalletStateMutationRequested,
"lite backend artifact contract cannot mutate WalletState");
}
if (input.walletPersistenceRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::WalletPersistenceRequested,
"lite backend artifact contract cannot persist wallet files");
}
if (input.uploadRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::UploadRequested,
"lite backend artifact contract cannot upload artifacts");
}
if (input.signingRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::SigningRequested,
"lite backend artifact contract cannot sign artifacts");
}
if (input.publicationRequested) {
return stoppedResult(std::move(result), Status::RuntimeActionDisabled, Issue::PublicationRequested,
"lite backend artifact contract cannot publish artifacts");
}
return result;
}
} // namespace
const char* liteBackendArtifactContractSupportedAbiVersion()
{
return kSupportedAbiVersion;
}
const char* liteBackendArtifactContractLinkModeName(LiteBackendArtifactContractLinkMode linkMode)
{
switch (linkMode) {
case LiteBackendArtifactContractLinkMode::ImportedLibrary: return "ImportedLibrary";
case LiteBackendArtifactContractLinkMode::RuntimeDynamicLibrary: return "RuntimeDynamicLibrary";
case LiteBackendArtifactContractLinkMode::ExternalExecutable: return "ExternalExecutable";
}
return "Unknown";
}
const char* liteBackendArtifactContractStatusName(LiteBackendArtifactContractStatus status)
{
switch (status) {
case LiteBackendArtifactContractStatus::ReadyForResolver: return "ReadyForResolver";
case LiteBackendArtifactContractStatus::WaitingForContractOwner: return "WaitingForContractOwner";
case LiteBackendArtifactContractStatus::WaitingForReadOnlyGate: return "WaitingForReadOnlyGate";
case LiteBackendArtifactContractStatus::WaitingForLinkMode: return "WaitingForLinkMode";
case LiteBackendArtifactContractStatus::WaitingForArtifactPath: return "WaitingForArtifactPath";
case LiteBackendArtifactContractStatus::WaitingForArtifactKind: return "WaitingForArtifactKind";
case LiteBackendArtifactContractStatus::WaitingForAbiVersion: return "WaitingForAbiVersion";
case LiteBackendArtifactContractStatus::WaitingForSymbolInventory: return "WaitingForSymbolInventory";
case LiteBackendArtifactContractStatus::WaitingForRequiredSymbols: return "WaitingForRequiredSymbols";
case LiteBackendArtifactContractStatus::WaitingForSignatureMetadata: return "WaitingForSignatureMetadata";
case LiteBackendArtifactContractStatus::RuntimeActionDisabled: return "RuntimeActionDisabled";
}
return "Unknown";
}
const char* liteBackendArtifactContractIssueName(LiteBackendArtifactContractIssue issue)
{
switch (issue) {
#define DRAGONX_CASE(value) case LiteBackendArtifactContractIssue::value: return #value
DRAGONX_CASE(ContractOwnerMissing);
DRAGONX_CASE(ReadOnlyGateMissing);
DRAGONX_CASE(RuntimeDynamicLinkDeferred);
DRAGONX_CASE(ExternalExecutableDeferred);
DRAGONX_CASE(ArtifactPathMissing);
DRAGONX_CASE(ArtifactKindNotLinkable);
DRAGONX_CASE(AbiVersionMissing);
DRAGONX_CASE(AbiVersionUnsupported);
DRAGONX_CASE(SymbolInventoryOwnerMissing);
DRAGONX_CASE(SymbolInventoryMissing);
DRAGONX_CASE(RequiredSymbolMissing);
DRAGONX_CASE(SignaturePolicyMissing);
DRAGONX_CASE(SignatureMetadataMissing);
DRAGONX_CASE(SignatureMetadataIncomplete);
DRAGONX_CASE(SignatureVerificationMissing);
DRAGONX_CASE(SignatureVerificationFailed);
DRAGONX_CASE(SignatureVerifiedArtifactShaMismatch);
DRAGONX_CASE(SignatureTrustMetadataMissing);
DRAGONX_CASE(ArtifactMutationRequested);
DRAGONX_CASE(DynamicLibraryLoadRequested);
DRAGONX_CASE(DynamicLibraryUnloadRequested);
DRAGONX_CASE(SymbolResolutionRequested);
DRAGONX_CASE(SdxlApiRequested);
DRAGONX_CASE(BridgeCallRequested);
DRAGONX_CASE(ServerConnectivityCheckRequested);
DRAGONX_CASE(WalletLifecycleRequested);
DRAGONX_CASE(SyncRequested);
DRAGONX_CASE(SyncStatusPollingRequested);
DRAGONX_CASE(WorkerQueueRequested);
DRAGONX_CASE(WalletStateMutationRequested);
DRAGONX_CASE(WalletPersistenceRequested);
DRAGONX_CASE(UploadRequested);
DRAGONX_CASE(SigningRequested);
DRAGONX_CASE(PublicationRequested);
#undef DRAGONX_CASE
}
return "Unknown";
}
std::vector<LiteBackendArtifactContractSymbol> liteBackendArtifactContractRequiredSymbols()
{
return {
{"walletExists", "litelib_wallet_exists", false, false, true, false, false, false},
{"initializeNew", "litelib_initialize_new", true, false, true, false, false, false},
{"initializeNewFromPhrase", "litelib_initialize_new_from_phrase", true, false, true, false, false, false},
{"initializeExisting", "litelib_initialize_existing", true, false, true, false, false, false},
{"execute", "litelib_execute", true, false, false, true, false, false},
{"freeString", "litelib_rust_free_string", false, true, true, true, false, false},
{"checkServerOnline", "litelib_check_server_online", false, false, false, false, true, false},
{"shutdown", "litelib_shutdown", false, false, true, true, true, true},
};
}
LiteWalletSdxlArtifactSymbolsInput liteBackendArtifactSymbolsFromExportedNames(
const std::vector<std::string>& exportedSymbols)
{
LiteWalletSdxlArtifactSymbolsInput symbols;
symbols.walletExists = containsSymbol(exportedSymbols, "litelib_wallet_exists");
symbols.initializeNew = containsSymbol(exportedSymbols, "litelib_initialize_new");
symbols.initializeNewFromPhrase = containsSymbol(exportedSymbols, "litelib_initialize_new_from_phrase");
symbols.initializeExisting = containsSymbol(exportedSymbols, "litelib_initialize_existing");
symbols.execute = containsSymbol(exportedSymbols, "litelib_execute");
symbols.freeString = containsSymbol(exportedSymbols, "litelib_rust_free_string");
symbols.checkServerOnline = containsSymbol(exportedSymbols, "litelib_check_server_online");
symbols.shutdown = containsSymbol(exportedSymbols, "litelib_shutdown");
return symbols;
}
LiteBackendArtifactContractResult evaluateLiteBackendArtifactContract(
const LiteBackendArtifactContractInput& input,
LiteBackendArtifactContractOptions options)
{
LiteBackendArtifactContractResult result;
result.requiredSymbolCount = liteBackendArtifactContractRequiredSymbols().size();
result.exportedSymbolCount = input.exportedSymbols.size();
result = rejectRuntimeActions(std::move(result), input, options);
if (!result.issues.empty()) return result;
if (options.requireContractOwner && !input.contractOwnerReady) {
return stoppedResult(std::move(result), Status::WaitingForContractOwner, Issue::ContractOwnerMissing,
"lite backend artifact contract owner is not ready");
}
result.contractOwnerAccepted = true;
if (options.requireReadOnlyGate && !input.readOnlyGateReady) {
return stoppedResult(std::move(result), Status::WaitingForReadOnlyGate, Issue::ReadOnlyGateMissing,
"lite backend artifact contract read-only gate is not ready");
}
result.readOnlyGateAccepted = true;
if (options.requireImportedLibraryLinkMode && input.linkMode == LiteBackendArtifactContractLinkMode::RuntimeDynamicLibrary) {
return stoppedResult(std::move(result), Status::WaitingForLinkMode, Issue::RuntimeDynamicLinkDeferred,
"runtime dynamic loading is deferred to the bridge runtime owner phase");
}
if (options.requireImportedLibraryLinkMode && input.linkMode == LiteBackendArtifactContractLinkMode::ExternalExecutable) {
return stoppedResult(std::move(result), Status::WaitingForLinkMode, Issue::ExternalExecutableDeferred,
"external executable bridge mode is not the Phase 1 production link contract");
}
result.importedLinkModeAccepted = input.linkMode == LiteBackendArtifactContractLinkMode::ImportedLibrary;
if (trimCopy(input.artifactPath).empty()) {
return stoppedResult(std::move(result), Status::WaitingForArtifactPath, Issue::ArtifactPathMissing,
"lite backend artifact contract requires an artifact path");
}
result.artifactPathAccepted = true;
if (!importedArtifactKindLinkable(input.artifactKind)) {
return stoppedResult(std::move(result), Status::WaitingForArtifactKind, Issue::ArtifactKindNotLinkable,
"Phase 1 imported link mode requires a static or shared library artifact");
}
result.artifactKindAccepted = true;
if (options.requireAbiVersion && trimCopy(input.abiVersion).empty()) {
return stoppedResult(std::move(result), Status::WaitingForAbiVersion, Issue::AbiVersionMissing,
"lite backend artifact ABI version is missing");
}
if (options.requireAbiVersion && input.abiVersion != kSupportedAbiVersion) {
return stoppedResult(std::move(result), Status::WaitingForAbiVersion, Issue::AbiVersionUnsupported,
"lite backend artifact ABI version is unsupported");
}
result.abiVersionAccepted = true;
result = validateSignatureMetadata(std::move(result), input, options);
if (!result.issues.empty()) return result;
if (options.requireSymbolInventory && !input.symbolInventoryOwnerReady) {
return stoppedResult(std::move(result), Status::WaitingForSymbolInventory, Issue::SymbolInventoryOwnerMissing,
"lite backend artifact symbol inventory owner is not ready");
}
if (options.requireSymbolInventory && input.exportedSymbols.empty()) {
return stoppedResult(std::move(result), Status::WaitingForSymbolInventory, Issue::SymbolInventoryMissing,
"lite backend artifact exported symbol inventory is missing");
}
result.symbolInventoryAccepted = true;
result.symbols = liteBackendArtifactSymbolsFromExportedNames(input.exportedSymbols);
for (const auto& symbol : liteBackendArtifactContractRequiredSymbols()) {
if (!containsSymbol(input.exportedSymbols, symbol.abiName.c_str())) {
result.missingSymbols.push_back(symbol.abiName);
}
}
if (options.requireAllRequiredSymbols && !result.missingSymbols.empty()) {
return stoppedResult(std::move(result), Status::WaitingForRequiredSymbols, Issue::RequiredSymbolMissing,
"lite backend artifact is missing required C ABI symbols");
}
result.requiredSymbolsAccepted = true;
result.resolverCandidate.configured = true;
result.resolverCandidate.artifactPath = input.artifactPath;
result.resolverCandidate.platform = input.platform;
result.resolverCandidate.kind = input.artifactKind;
result.resolverCandidate.versionLabel = input.abiVersion;
result.resolverCandidate.provenance = input.provenance;
result.resolverCandidate.signatureVerification = input.signatureVerification;
result.resolverCandidate.sdxlCompatible = input.sdxlCompatible;
result.resolverCandidate.symbols = result.symbols;
result.resolverCandidate.executableMetadataRequired = false;
result.resolverInput.resolverOwnerReady = true;
result.resolverInput.readOnlyGateReady = true;
result.resolverInput.projectRoot = input.projectRoot;
result.resolverInput.expectedPlatform = input.platform;
result.resolverInput.candidates.push_back(result.resolverCandidate);
result.resolverInputProduced = true;
result.provenanceForwarded = input.provenance.ownerReady && input.provenance.metadataProvided;
result.signatureVerificationForwarded = input.signatureVerification.metadataProvided;
result.sdxlCompatibilityForwarded = input.sdxlCompatible;
result.dynamicLibraryLoadAllowed = false;
result.symbolResolutionAllowed = false;
result.runtimeActivationAllowed = false;
result.ok = true;
result.status = Status::ReadyForResolver;
std::ostringstream summary;
summary << "lite_backend_artifact_contract=ready"
<< ";abi=" << input.abiVersion
<< ";link_mode=" << liteBackendArtifactContractLinkModeName(input.linkMode)
<< ";kind=" << liteBackendArtifactKindName(input.artifactKind)
<< ";symbols=" << result.requiredSymbolCount
<< ";signature_required=" << (result.signatureRequiredForRelease ? "true" : "false")
<< ";signature_verified=" << (result.signatureVerified ? "true" : "false")
<< ";dynamic_loading=false;runtime_activation=false";
result.summary = summary.str();
return result;
}
LiteBackendArtifactResolverInput liteBackendArtifactResolverInputFromContractResult(
const LiteBackendArtifactContractResult& result)
{
return result.resolverInput;
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,195 @@
#pragma once
#include "lite_backend_artifact_resolver.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteBackendArtifactContractLinkMode {
ImportedLibrary,
RuntimeDynamicLibrary,
ExternalExecutable,
};
enum class LiteBackendArtifactContractStatus {
ReadyForResolver,
WaitingForContractOwner,
WaitingForReadOnlyGate,
WaitingForLinkMode,
WaitingForArtifactPath,
WaitingForArtifactKind,
WaitingForAbiVersion,
WaitingForSymbolInventory,
WaitingForRequiredSymbols,
WaitingForSignatureMetadata,
RuntimeActionDisabled,
};
enum class LiteBackendArtifactContractIssue {
ContractOwnerMissing,
ReadOnlyGateMissing,
RuntimeDynamicLinkDeferred,
ExternalExecutableDeferred,
ArtifactPathMissing,
ArtifactKindNotLinkable,
AbiVersionMissing,
AbiVersionUnsupported,
SymbolInventoryOwnerMissing,
SymbolInventoryMissing,
RequiredSymbolMissing,
SignaturePolicyMissing,
SignatureMetadataMissing,
SignatureMetadataIncomplete,
SignatureVerificationMissing,
SignatureVerificationFailed,
SignatureVerifiedArtifactShaMismatch,
SignatureTrustMetadataMissing,
ArtifactMutationRequested,
DynamicLibraryLoadRequested,
DynamicLibraryUnloadRequested,
SymbolResolutionRequested,
SdxlApiRequested,
BridgeCallRequested,
ServerConnectivityCheckRequested,
WalletLifecycleRequested,
SyncRequested,
SyncStatusPollingRequested,
WorkerQueueRequested,
WalletStateMutationRequested,
WalletPersistenceRequested,
UploadRequested,
SigningRequested,
PublicationRequested,
};
struct LiteBackendArtifactContractSymbol {
std::string logicalName;
std::string abiName;
bool returnsOwnedString = false;
bool cleanupFunction = false;
bool requiredForLifecycle = false;
bool requiredForSync = false;
bool requiredForServerCheck = false;
bool requiredForShutdown = false;
};
struct LiteBackendArtifactContractInput {
bool contractOwnerReady = false;
bool readOnlyGateReady = false;
std::string projectRoot;
std::string artifactPath;
LiteBackendArtifactPlatform platform = LiteBackendArtifactPlatform::Current;
LiteBackendArtifactKind artifactKind = LiteBackendArtifactKind::Unknown;
LiteBackendArtifactContractLinkMode linkMode = LiteBackendArtifactContractLinkMode::ImportedLibrary;
std::string abiVersion;
std::string artifactSha256;
LiteBackendArtifactProvenanceMetadata provenance;
LiteBackendArtifactSignatureVerificationMetadata signatureVerification;
bool sdxlCompatible = false;
bool symbolInventoryOwnerReady = false;
std::vector<std::string> exportedSymbols;
bool artifactMutationRequested = false;
bool dynamicLibraryLoadRequested = false;
bool dynamicLibraryUnloadRequested = false;
bool symbolResolutionRequested = false;
bool sdxlApiRequested = false;
bool bridgeCallRequested = false;
bool serverConnectivityCheckRequested = false;
bool walletLifecycleRequested = false;
bool syncRequested = false;
bool syncStatusPollingRequested = false;
bool workerQueueRequested = false;
bool walletStateMutationRequested = false;
bool walletPersistenceRequested = false;
bool uploadRequested = false;
bool signingRequested = false;
bool publicationRequested = false;
};
struct LiteBackendArtifactContractOptions {
bool requireContractOwner = true;
bool requireReadOnlyGate = true;
bool requireImportedLibraryLinkMode = true;
bool requireAbiVersion = true;
bool requireSymbolInventory = true;
bool requireAllRequiredSymbols = true;
bool requireSignatureMetadataWhenRequired = true;
bool validateOptionalSignatureMetadata = true;
bool rejectRuntimeActions = true;
};
struct LiteBackendArtifactContractIssueInfo {
LiteBackendArtifactContractIssue issue = LiteBackendArtifactContractIssue::ContractOwnerMissing;
std::string message;
};
struct LiteBackendArtifactContractResult {
bool ok = false;
bool readOnlyContract = true;
bool noArtifactMutation = true;
bool noDynamicLibraryLoaded = true;
bool noDynamicLibraryUnloaded = true;
bool noSymbolResolutionPerformed = true;
bool noSdxlCalls = true;
bool noBridgeCalls = true;
bool noServerConnectivityChecked = true;
bool noWalletLifecycle = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWorkerQueueEnqueue = true;
bool noWalletStateMutation = true;
bool noWalletPersistence = true;
bool noUpload = true;
bool noSigning = true;
bool noPublication = true;
bool contractOwnerAccepted = false;
bool readOnlyGateAccepted = false;
bool importedLinkModeAccepted = false;
bool artifactPathAccepted = false;
bool artifactKindAccepted = false;
bool abiVersionAccepted = false;
bool provenanceForwarded = false;
bool signatureMetadataAccepted = false;
bool signatureVerificationForwarded = false;
bool signatureRequiredForRelease = false;
bool signatureVerified = false;
bool sdxlCompatibilityForwarded = false;
bool symbolInventoryAccepted = false;
bool requiredSymbolsAccepted = false;
bool resolverInputProduced = false;
bool dynamicLibraryLoadAllowed = false;
bool symbolResolutionAllowed = false;
bool runtimeActivationAllowed = false;
std::size_t requiredSymbolCount = 0;
std::size_t exportedSymbolCount = 0;
LiteBackendArtifactContractStatus status = LiteBackendArtifactContractStatus::WaitingForContractOwner;
LiteWalletSdxlArtifactSymbolsInput symbols;
LiteBackendArtifactCandidate resolverCandidate;
LiteBackendArtifactResolverInput resolverInput;
std::vector<std::string> missingSymbols;
std::vector<LiteBackendArtifactContractIssueInfo> issues;
std::string error;
std::string summary;
};
const char* liteBackendArtifactContractSupportedAbiVersion();
const char* liteBackendArtifactContractLinkModeName(LiteBackendArtifactContractLinkMode linkMode);
const char* liteBackendArtifactContractStatusName(LiteBackendArtifactContractStatus status);
const char* liteBackendArtifactContractIssueName(LiteBackendArtifactContractIssue issue);
std::vector<LiteBackendArtifactContractSymbol> liteBackendArtifactContractRequiredSymbols();
LiteWalletSdxlArtifactSymbolsInput liteBackendArtifactSymbolsFromExportedNames(
const std::vector<std::string>& exportedSymbols);
LiteBackendArtifactContractResult evaluateLiteBackendArtifactContract(
const LiteBackendArtifactContractInput& input,
LiteBackendArtifactContractOptions options = {});
LiteBackendArtifactResolverInput liteBackendArtifactResolverInputFromContractResult(
const LiteBackendArtifactContractResult& result);
} // namespace dragonx::wallet

View File

@@ -0,0 +1,925 @@
#include "wallet/lite_backend_artifact_resolver.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <system_error>
#include <utility>
namespace dragonx::wallet {
namespace {
namespace fs = std::filesystem;
struct CandidateInspectionResult {
bool ok = false;
LiteBackendArtifactResolverStatus status = LiteBackendArtifactResolverStatus::WaitingForArtifactCandidate;
LiteBackendArtifactResolverIssue issue = LiteBackendArtifactResolverIssue::ArtifactMissing;
std::string message;
LiteBackendResolvedArtifact artifact;
LiteWalletSdxlArtifactInput syncArtifactInput;
};
void addIssue(LiteBackendArtifactResolverResult& result,
LiteBackendArtifactResolverIssue issue,
std::string message)
{
result.issues.push_back(LiteBackendArtifactResolverIssueInfo{issue, std::move(message)});
}
void addIssue(LiteBackendActivationReadinessResult& result,
LiteBackendActivationReadinessIssue issue,
std::string message)
{
result.issues.push_back(LiteBackendActivationReadinessIssueInfo{issue, std::move(message)});
}
LiteBackendArtifactResolverResult stoppedResolverResult(
LiteBackendArtifactResolverResult result,
LiteBackendArtifactResolverStatus status,
LiteBackendArtifactResolverIssue issue,
std::string message)
{
result.status = status;
addIssue(result, issue, std::move(message));
result.error = result.issues.back().message;
return result;
}
LiteBackendActivationReadinessResult stoppedActivationResult(
LiteBackendActivationReadinessResult result,
LiteBackendActivationReadinessStatus status,
LiteBackendActivationReadinessIssue issue,
std::string message,
WalletBackendState backendState = WalletBackendState::Unavailable)
{
result.status = status;
addIssue(result, issue, std::move(message));
result.error = result.issues.back().message;
result.connectionStatus = WalletBackendStatus{backendState, result.error, {}, {}, 0.0};
return result;
}
std::string lowerCopy(const std::string& value)
{
std::string lowered;
lowered.reserve(value.size());
for (const unsigned char character : value) {
lowered.push_back(static_cast<char>(std::tolower(character)));
}
return lowered;
}
std::string trimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
std::string baseNameLower(const std::string& path)
{
const auto slash = path.find_last_of("/\\");
const std::string name = slash == std::string::npos ? path : path.substr(slash + 1);
return lowerCopy(name);
}
bool endsWith(const std::string& value, const char* suffix)
{
const std::string suffixValue(suffix);
return value.size() >= suffixValue.size() &&
value.compare(value.size() - suffixValue.size(), suffixValue.size(), suffixValue) == 0;
}
LiteBackendArtifactPlatform normalizePlatform(LiteBackendArtifactPlatform platform)
{
return platform == LiteBackendArtifactPlatform::Current
? currentLiteBackendArtifactPlatform()
: platform;
}
LiteBackendArtifactKind inferArtifactKind(const std::string& path)
{
const std::string name = baseNameLower(path);
if (endsWith(name, ".a") || endsWith(name, ".lib")) return LiteBackendArtifactKind::StaticLibrary;
if (endsWith(name, ".so") || endsWith(name, ".dll") || endsWith(name, ".dylib")) {
return LiteBackendArtifactKind::SharedLibrary;
}
if (endsWith(name, ".exe") || name == "silentdragonxlite" || name == "silentdragonx-lite") {
return LiteBackendArtifactKind::Executable;
}
return LiteBackendArtifactKind::Unknown;
}
bool artifactKindSupported(LiteBackendArtifactKind kind)
{
return kind == LiteBackendArtifactKind::StaticLibrary ||
kind == LiteBackendArtifactKind::SharedLibrary ||
kind == LiteBackendArtifactKind::Executable;
}
std::vector<std::string> defaultArtifactNames(LiteBackendArtifactPlatform platform,
LiteBackendArtifactKind kind)
{
const auto normalizedPlatform = normalizePlatform(platform);
std::vector<std::string> names;
const auto addNamesForKind = [&](LiteBackendArtifactKind artifactKind) {
switch (normalizedPlatform) {
case LiteBackendArtifactPlatform::Windows:
if (artifactKind == LiteBackendArtifactKind::StaticLibrary) {
names.push_back("silentdragonxlite.lib");
names.push_back("libsilentdragonxlite.a");
} else if (artifactKind == LiteBackendArtifactKind::SharedLibrary) {
names.push_back("silentdragonxlite.dll");
} else if (artifactKind == LiteBackendArtifactKind::Executable) {
names.push_back("silentdragonxlite.exe");
names.push_back("silentdragonx-lite.exe");
}
break;
case LiteBackendArtifactPlatform::MacOS:
if (artifactKind == LiteBackendArtifactKind::StaticLibrary) {
names.push_back("libsilentdragonxlite.a");
names.push_back("silentdragonxlite.a");
} else if (artifactKind == LiteBackendArtifactKind::SharedLibrary) {
names.push_back("libsilentdragonxlite.dylib");
names.push_back("silentdragonxlite.dylib");
} else if (artifactKind == LiteBackendArtifactKind::Executable) {
names.push_back("silentdragonxlite");
names.push_back("silentdragonx-lite");
}
break;
case LiteBackendArtifactPlatform::Linux:
case LiteBackendArtifactPlatform::Current:
case LiteBackendArtifactPlatform::Unknown:
if (artifactKind == LiteBackendArtifactKind::StaticLibrary) {
names.push_back("silentdragonxlite.a");
names.push_back("libsilentdragonxlite.a");
} else if (artifactKind == LiteBackendArtifactKind::SharedLibrary) {
names.push_back("libsilentdragonxlite.so");
names.push_back("silentdragonxlite.so");
} else if (artifactKind == LiteBackendArtifactKind::Executable) {
names.push_back("silentdragonxlite");
names.push_back("silentdragonx-lite");
}
break;
}
};
if (kind == LiteBackendArtifactKind::Unknown) {
addNamesForKind(LiteBackendArtifactKind::StaticLibrary);
addNamesForKind(LiteBackendArtifactKind::SharedLibrary);
addNamesForKind(LiteBackendArtifactKind::Executable);
} else {
addNamesForKind(kind);
}
return names;
}
fs::path resolvePath(const std::string& projectRoot, const std::string& path)
{
const fs::path value(path);
if (value.is_absolute()) return value;
const fs::path root = projectRoot.empty() ? fs::path(".") : fs::path(projectRoot);
return root / value;
}
std::vector<LiteBackendArtifactCandidate> expandSearchRoots(
const LiteBackendArtifactResolverInput& input,
LiteBackendArtifactResolverResult& result,
LiteBackendArtifactResolverOptions options)
{
std::vector<LiteBackendArtifactCandidate> candidates;
result.searchRootCount = input.searchRoots.size();
for (const auto& root : input.searchRoots) {
if (options.requireArtifactCandidates && !root.configured) {
result = stoppedResolverResult(
std::move(result),
LiteBackendArtifactResolverStatus::WaitingForArtifactSearchRoot,
LiteBackendArtifactResolverIssue::ArtifactSearchRootNotConfigured,
"lite backend artifact search root is not configured");
return {};
}
if (trimCopy(root.rootPath).empty()) {
result = stoppedResolverResult(
std::move(result),
LiteBackendArtifactResolverStatus::WaitingForArtifactSearchRoot,
LiteBackendArtifactResolverIssue::ArtifactSearchRootMissing,
"lite backend artifact search root path is missing");
return {};
}
const fs::path rootPath = resolvePath(input.projectRoot, root.rootPath);
std::error_code error;
if (!fs::exists(rootPath, error) || error) {
result = stoppedResolverResult(
std::move(result),
LiteBackendArtifactResolverStatus::WaitingForArtifactSearchRoot,
LiteBackendArtifactResolverIssue::ArtifactSearchRootMissing,
"lite backend artifact search root does not exist");
return {};
}
if (!fs::is_directory(rootPath, error) || error) {
result = stoppedResolverResult(
std::move(result),
LiteBackendArtifactResolverStatus::WaitingForArtifactSearchRoot,
LiteBackendArtifactResolverIssue::ArtifactSearchRootNotDirectory,
"lite backend artifact search root is not a directory");
return {};
}
const auto names = root.expectedArtifactNames.empty()
? defaultArtifactNames(root.platform, root.kind)
: root.expectedArtifactNames;
for (const auto& artifactName : names) {
LiteBackendArtifactCandidate candidate;
candidate.configured = true;
candidate.artifactPath = (rootPath / artifactName).string();
candidate.platform = root.platform;
candidate.kind = root.kind;
candidate.versionLabel = root.versionLabel;
candidate.provenance = root.provenance;
candidate.signatureVerification = root.signatureVerification;
candidate.sdxlCompatible = root.sdxlCompatible;
candidate.symbols = root.symbols;
candidate.executableMetadataRequired = root.executableMetadataRequired;
candidates.push_back(std::move(candidate));
}
}
result.artifactSearchRootsAccepted = true;
return candidates;
}
bool coreSymbolsReady(const LiteWalletSdxlArtifactSymbolsInput& symbols)
{
return symbols.walletExists &&
symbols.initializeNew &&
symbols.initializeNewFromPhrase &&
symbols.initializeExisting &&
symbols.execute &&
symbols.checkServerOnline;
}
bool fileReadable(const fs::path& path)
{
std::ifstream input(path, std::ios::binary);
return input.good();
}
bool fileExecutable(const fs::path& path, LiteBackendArtifactPlatform platform)
{
const std::string name = baseNameLower(path.string());
if (normalizePlatform(platform) == LiteBackendArtifactPlatform::Windows && endsWith(name, ".exe")) return true;
std::error_code error;
const auto permissions = fs::status(path, error).permissions();
if (error) return false;
return (permissions & fs::perms::owner_exec) != fs::perms::none ||
(permissions & fs::perms::group_exec) != fs::perms::none ||
(permissions & fs::perms::others_exec) != fs::perms::none;
}
bool provenanceComplete(const LiteBackendArtifactProvenanceMetadata& provenance)
{
return provenance.metadataProvided &&
!trimCopy(provenance.source).empty() &&
!trimCopy(provenance.builder).empty() &&
!trimCopy(provenance.sourceRevision).empty() &&
!trimCopy(provenance.artifactSetId).empty();
}
CandidateInspectionResult inspectCandidate(const LiteBackendArtifactResolverInput& input,
const LiteBackendArtifactCandidate& candidate,
LiteBackendArtifactResolverOptions options)
{
CandidateInspectionResult inspection;
if (options.requireArtifactCandidates && !candidate.configured) {
inspection.issue = LiteBackendArtifactResolverIssue::ArtifactCandidateNotConfigured;
inspection.message = "lite backend artifact candidate is not configured";
return inspection;
}
if (trimCopy(candidate.artifactPath).empty()) {
inspection.issue = LiteBackendArtifactResolverIssue::ArtifactPathMissing;
inspection.message = "lite backend artifact candidate path is missing";
return inspection;
}
const fs::path artifactPath = resolvePath(input.projectRoot, candidate.artifactPath);
std::error_code error;
if (!fs::exists(artifactPath, error) || error) {
inspection.issue = LiteBackendArtifactResolverIssue::ArtifactMissing;
inspection.message = "lite backend artifact candidate does not exist";
return inspection;
}
if (!fs::is_regular_file(artifactPath, error) || error) {
inspection.issue = LiteBackendArtifactResolverIssue::ArtifactNotRegularFile;
inspection.message = "lite backend artifact candidate is not a regular file";
return inspection;
}
if (!fileReadable(artifactPath)) {
inspection.issue = LiteBackendArtifactResolverIssue::ArtifactUnreadable;
inspection.message = "lite backend artifact candidate is not readable";
return inspection;
}
const auto candidatePlatform = normalizePlatform(candidate.platform);
const auto expectedPlatform = normalizePlatform(input.expectedPlatform);
if (candidatePlatform == LiteBackendArtifactPlatform::Unknown || candidatePlatform != expectedPlatform) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::PlatformMismatch;
inspection.message = "lite backend artifact platform metadata does not match the expected platform";
return inspection;
}
const auto kind = candidate.kind == LiteBackendArtifactKind::Unknown
? inferArtifactKind(candidate.artifactPath)
: candidate.kind;
if (!artifactKindSupported(kind)) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::UnsupportedArtifactKind;
inspection.message = "lite backend artifact kind is unsupported or unknown";
return inspection;
}
if (options.requireVersionMetadata && trimCopy(candidate.versionLabel).empty()) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::VersionMissing;
inspection.message = "lite backend artifact version metadata is missing";
return inspection;
}
if (options.requireProvenanceMetadata) {
if (!candidate.provenance.ownerReady) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::ProvenanceOwnerMissing;
inspection.message = "lite backend artifact provenance owner is not ready";
return inspection;
}
if (!provenanceComplete(candidate.provenance)) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::ProvenanceMetadataMissing;
inspection.message = "lite backend artifact provenance metadata is incomplete";
return inspection;
}
}
if (options.requireSdxlCompatibility && !candidate.sdxlCompatible) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::ArtifactNotSdxlCompatible;
inspection.message = "lite backend artifact is not marked SDXL-compatible";
return inspection;
}
if (options.requireSdxlSymbols) {
if (!coreSymbolsReady(candidate.symbols)) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::ArtifactSymbolsMissing;
inspection.message = "lite backend artifact is missing required SDXL bridge symbol metadata";
return inspection;
}
if (!candidate.symbols.freeString) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::ArtifactStringOwnershipUnverified;
inspection.message = "lite backend artifact string cleanup symbol metadata is missing";
return inspection;
}
if (!candidate.symbols.shutdown) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::ArtifactShutdownUnavailable;
inspection.message = "lite backend artifact shutdown symbol metadata is missing";
return inspection;
}
}
const bool executable = fileExecutable(artifactPath, candidatePlatform);
if ((kind == LiteBackendArtifactKind::Executable || candidate.executableMetadataRequired) && !executable) {
inspection.status = LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata;
inspection.issue = LiteBackendArtifactResolverIssue::ExecutableMetadataMissing;
inspection.message = "lite backend executable artifact is not marked executable";
return inspection;
}
std::size_t artifactSizeBytes = 0;
const auto fileSize = fs::file_size(artifactPath, error);
if (!error) artifactSizeBytes = static_cast<std::size_t>(fileSize);
inspection.ok = true;
inspection.artifact.found = true;
inspection.artifact.artifactPath = artifactPath.string();
inspection.artifact.platform = candidatePlatform;
inspection.artifact.kind = kind;
inspection.artifact.versionLabel = candidate.versionLabel;
inspection.artifact.provenance = candidate.provenance;
inspection.artifact.signatureVerification = candidate.signatureVerification;
inspection.artifact.exists = true;
inspection.artifact.regularFile = true;
inspection.artifact.readable = true;
inspection.artifact.executable = executable;
inspection.artifact.sdxlCompatible = candidate.sdxlCompatible;
inspection.artifact.symbols = candidate.symbols;
inspection.artifact.artifactSizeBytes = artifactSizeBytes;
inspection.syncArtifactInput.pathConfigured = true;
inspection.syncArtifactInput.exists = true;
inspection.syncArtifactInput.readable = true;
inspection.syncArtifactInput.sdxlCompatible = candidate.sdxlCompatible;
inspection.syncArtifactInput.artifactPath = artifactPath.string();
inspection.syncArtifactInput.versionLabel = candidate.versionLabel;
inspection.syncArtifactInput.symbols = candidate.symbols;
return inspection;
}
LiteBackendArtifactResolverResult rejectResolverRuntimeAction(
LiteBackendArtifactResolverResult result,
const LiteBackendArtifactResolverInput& input,
LiteBackendArtifactResolverOptions options)
{
if (options.rejectArtifactMutation && input.artifactMutationRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::ArtifactMutationRequested,
"lite backend artifact mutation is disabled for artifact resolution");
}
if (options.rejectSdxlApiCalls && input.sdxlApiRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::SdxlApiRequested,
"SDXL API calls are disabled for artifact resolution");
}
if (options.rejectServerConnectivityChecks && input.serverConnectivityCheckRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::ServerConnectivityCheckRequested,
"server connectivity checks are disabled for artifact resolution");
}
if (options.rejectWalletLifecycle && input.walletLifecycleRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::WalletLifecycleRequested,
"wallet lifecycle execution is disabled for artifact resolution");
}
if (options.rejectSync && input.syncRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::SyncRequested,
"lite sync execution is disabled for artifact resolution");
}
if (options.rejectSyncStatusPolling && input.syncStatusPollingRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::SyncStatusPollingRequested,
"lite syncstatus polling is disabled for artifact resolution");
}
if (options.rejectWorkerQueue && input.workerQueueRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::WorkerQueueRequested,
"worker queue enqueue is disabled for artifact resolution");
}
if (options.rejectWalletStateMutation && input.walletStateMutationRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::WalletStateMutationRequested,
"WalletState mutation is disabled for artifact resolution");
}
if (options.rejectWalletPersistence && input.walletPersistenceRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::WalletPersistenceRequested,
"wallet persistence is disabled for artifact resolution");
}
if (options.rejectUpload && input.uploadRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::UploadRequested,
"upload is disabled for artifact resolution");
}
if (options.rejectSigning && input.signingRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::SigningRequested,
"signing is disabled for artifact resolution");
}
if (options.rejectPublication && input.publicationRequested) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::RuntimeActionDisabled,
LiteBackendArtifactResolverIssue::PublicationRequested,
"publication is disabled for artifact resolution");
}
return result;
}
LiteBackendActivationReadinessResult rejectActivationRuntimeAction(
LiteBackendActivationReadinessResult result,
const LiteBackendActivationReadinessInput& input,
LiteBackendActivationReadinessOptions options)
{
if (options.rejectArtifactMutation && input.artifactMutationRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::ArtifactMutationRequested,
"lite backend artifact mutation is disabled for activation readiness");
}
if (options.rejectSdxlApiCalls && input.sdxlApiRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::SdxlApiRequested,
"SDXL API calls are disabled for activation readiness");
}
if (options.rejectServerConnectivityChecks && input.serverConnectivityCheckRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::ServerConnectivityCheckRequested,
"server connectivity checks are disabled for activation readiness");
}
if (options.rejectWalletLifecycle && input.walletLifecycleRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::WalletLifecycleRequested,
"wallet lifecycle execution is disabled for activation readiness");
}
if (options.rejectSync && input.syncRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::SyncRequested,
"lite sync execution is disabled for activation readiness");
}
if (options.rejectSyncStatusPolling && input.syncStatusPollingRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::SyncStatusPollingRequested,
"lite syncstatus polling is disabled for activation readiness");
}
if (options.rejectWorkerQueue && input.workerQueueRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::WorkerQueueRequested,
"worker queue enqueue is disabled for activation readiness");
}
if (options.rejectWalletStateMutation && input.walletStateMutationRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::WalletStateMutationRequested,
"WalletState mutation is disabled for activation readiness");
}
if (options.rejectWalletPersistence && input.walletPersistenceRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::WalletPersistenceRequested,
"wallet persistence is disabled for activation readiness");
}
if (options.rejectUpload && input.uploadRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::UploadRequested,
"upload is disabled for activation readiness");
}
if (options.rejectSigning && input.signingRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::SigningRequested,
"signing is disabled for activation readiness");
}
if (options.rejectPublication && input.publicationRequested) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::RuntimeActionDisabled,
LiteBackendActivationReadinessIssue::PublicationRequested,
"publication is disabled for activation readiness");
}
return result;
}
WalletBackendStatus backendStatusOrDefault(const LiteBackendBridgeReadinessInput& backend,
const std::string& fallback)
{
if (!backend.status.message.empty()) return backend.status;
return WalletBackendStatus{WalletBackendState::Unavailable, fallback, {}, {}, 0.0};
}
WalletBackendStatus readyConnectionStatus(const LiteServerSelectionResult& selectedServer)
{
const std::string serverLabel = selectedServer.server.label.empty()
? selectedServer.server.url
: selectedServer.server.label;
return WalletBackendStatus{
WalletBackendState::Disconnected,
"lite backend activation readiness accepted for " + serverLabel + "; bridge calls remain disabled here",
{},
{},
0.0
};
}
} // namespace
LiteBackendArtifactPlatform currentLiteBackendArtifactPlatform()
{
#if defined(_WIN32)
return LiteBackendArtifactPlatform::Windows;
#elif defined(__APPLE__)
return LiteBackendArtifactPlatform::MacOS;
#else
return LiteBackendArtifactPlatform::Linux;
#endif
}
const char* liteBackendArtifactPlatformName(LiteBackendArtifactPlatform platform)
{
switch (platform) {
case LiteBackendArtifactPlatform::Current: return "Current";
case LiteBackendArtifactPlatform::Linux: return "Linux";
case LiteBackendArtifactPlatform::Windows: return "Windows";
case LiteBackendArtifactPlatform::MacOS: return "MacOS";
case LiteBackendArtifactPlatform::Unknown: return "Unknown";
}
return "Unknown";
}
const char* liteBackendArtifactKindName(LiteBackendArtifactKind kind)
{
switch (kind) {
case LiteBackendArtifactKind::Unknown: return "Unknown";
case LiteBackendArtifactKind::StaticLibrary: return "StaticLibrary";
case LiteBackendArtifactKind::SharedLibrary: return "SharedLibrary";
case LiteBackendArtifactKind::Executable: return "Executable";
}
return "Unknown";
}
const char* liteBackendArtifactResolverStatusName(LiteBackendArtifactResolverStatus status)
{
switch (status) {
case LiteBackendArtifactResolverStatus::ReadyForActivationReadiness: return "ReadyForActivationReadiness";
case LiteBackendArtifactResolverStatus::WaitingForResolverOwner: return "WaitingForResolverOwner";
case LiteBackendArtifactResolverStatus::WaitingForReadOnlyGate: return "WaitingForReadOnlyGate";
case LiteBackendArtifactResolverStatus::WaitingForArtifactSearchRoot: return "WaitingForArtifactSearchRoot";
case LiteBackendArtifactResolverStatus::WaitingForArtifactCandidate: return "WaitingForArtifactCandidate";
case LiteBackendArtifactResolverStatus::WaitingForArtifactMetadata: return "WaitingForArtifactMetadata";
case LiteBackendArtifactResolverStatus::RuntimeActionDisabled: return "RuntimeActionDisabled";
}
return "Unknown";
}
const char* liteBackendArtifactResolverIssueName(LiteBackendArtifactResolverIssue issue)
{
switch (issue) {
case LiteBackendArtifactResolverIssue::ResolverOwnerMissing: return "ResolverOwnerMissing";
case LiteBackendArtifactResolverIssue::ReadOnlyGateMissing: return "ReadOnlyGateMissing";
case LiteBackendArtifactResolverIssue::ArtifactSearchRootMissing: return "ArtifactSearchRootMissing";
case LiteBackendArtifactResolverIssue::ArtifactSearchRootNotConfigured: return "ArtifactSearchRootNotConfigured";
case LiteBackendArtifactResolverIssue::ArtifactSearchRootNotDirectory: return "ArtifactSearchRootNotDirectory";
case LiteBackendArtifactResolverIssue::ArtifactCandidateMissing: return "ArtifactCandidateMissing";
case LiteBackendArtifactResolverIssue::ArtifactCandidateNotConfigured: return "ArtifactCandidateNotConfigured";
case LiteBackendArtifactResolverIssue::ArtifactPathMissing: return "ArtifactPathMissing";
case LiteBackendArtifactResolverIssue::ArtifactMissing: return "ArtifactMissing";
case LiteBackendArtifactResolverIssue::ArtifactNotRegularFile: return "ArtifactNotRegularFile";
case LiteBackendArtifactResolverIssue::ArtifactUnreadable: return "ArtifactUnreadable";
case LiteBackendArtifactResolverIssue::UnsupportedArtifactKind: return "UnsupportedArtifactKind";
case LiteBackendArtifactResolverIssue::PlatformMismatch: return "PlatformMismatch";
case LiteBackendArtifactResolverIssue::VersionMissing: return "VersionMissing";
case LiteBackendArtifactResolverIssue::ProvenanceOwnerMissing: return "ProvenanceOwnerMissing";
case LiteBackendArtifactResolverIssue::ProvenanceMetadataMissing: return "ProvenanceMetadataMissing";
case LiteBackendArtifactResolverIssue::ArtifactNotSdxlCompatible: return "ArtifactNotSdxlCompatible";
case LiteBackendArtifactResolverIssue::ArtifactSymbolsMissing: return "ArtifactSymbolsMissing";
case LiteBackendArtifactResolverIssue::ArtifactStringOwnershipUnverified: return "ArtifactStringOwnershipUnverified";
case LiteBackendArtifactResolverIssue::ArtifactShutdownUnavailable: return "ArtifactShutdownUnavailable";
case LiteBackendArtifactResolverIssue::ExecutableMetadataMissing: return "ExecutableMetadataMissing";
case LiteBackendArtifactResolverIssue::ArtifactMutationRequested: return "ArtifactMutationRequested";
case LiteBackendArtifactResolverIssue::SdxlApiRequested: return "SdxlApiRequested";
case LiteBackendArtifactResolverIssue::ServerConnectivityCheckRequested: return "ServerConnectivityCheckRequested";
case LiteBackendArtifactResolverIssue::WalletLifecycleRequested: return "WalletLifecycleRequested";
case LiteBackendArtifactResolverIssue::SyncRequested: return "SyncRequested";
case LiteBackendArtifactResolverIssue::SyncStatusPollingRequested: return "SyncStatusPollingRequested";
case LiteBackendArtifactResolverIssue::WorkerQueueRequested: return "WorkerQueueRequested";
case LiteBackendArtifactResolverIssue::WalletStateMutationRequested: return "WalletStateMutationRequested";
case LiteBackendArtifactResolverIssue::WalletPersistenceRequested: return "WalletPersistenceRequested";
case LiteBackendArtifactResolverIssue::UploadRequested: return "UploadRequested";
case LiteBackendArtifactResolverIssue::SigningRequested: return "SigningRequested";
case LiteBackendArtifactResolverIssue::PublicationRequested: return "PublicationRequested";
}
return "Unknown";
}
const char* liteBackendActivationReadinessStatusName(LiteBackendActivationReadinessStatus status)
{
switch (status) {
case LiteBackendActivationReadinessStatus::ReadyForConnectionReadiness: return "ReadyForConnectionReadiness";
case LiteBackendActivationReadinessStatus::WaitingForActivationOwner: return "WaitingForActivationOwner";
case LiteBackendActivationReadinessStatus::WaitingForReadOnlyGate: return "WaitingForReadOnlyGate";
case LiteBackendActivationReadinessStatus::WaitingForLiteBuild: return "WaitingForLiteBuild";
case LiteBackendActivationReadinessStatus::WaitingForBackendCapability: return "WaitingForBackendCapability";
case LiteBackendActivationReadinessStatus::WaitingForArtifactResolver: return "WaitingForArtifactResolver";
case LiteBackendActivationReadinessStatus::WaitingForBackendLink: return "WaitingForBackendLink";
case LiteBackendActivationReadinessStatus::WaitingForBridge: return "WaitingForBridge";
case LiteBackendActivationReadinessStatus::WaitingForConnectionSettings: return "WaitingForConnectionSettings";
case LiteBackendActivationReadinessStatus::RuntimeActionDisabled: return "RuntimeActionDisabled";
}
return "Unknown";
}
const char* liteBackendActivationReadinessIssueName(LiteBackendActivationReadinessIssue issue)
{
switch (issue) {
case LiteBackendActivationReadinessIssue::ActivationOwnerMissing: return "ActivationOwnerMissing";
case LiteBackendActivationReadinessIssue::ReadOnlyGateMissing: return "ReadOnlyGateMissing";
case LiteBackendActivationReadinessIssue::FullNodeBuild: return "FullNodeBuild";
case LiteBackendActivationReadinessIssue::LiteBackendCapabilityMissing: return "LiteBackendCapabilityMissing";
case LiteBackendActivationReadinessIssue::ArtifactResolverRejected: return "ArtifactResolverRejected";
case LiteBackendActivationReadinessIssue::BackendNotLinked: return "BackendNotLinked";
case LiteBackendActivationReadinessIssue::BridgeUnavailable: return "BridgeUnavailable";
case LiteBackendActivationReadinessIssue::ConnectionSettingsRejected: return "ConnectionSettingsRejected";
case LiteBackendActivationReadinessIssue::ArtifactMutationRequested: return "ArtifactMutationRequested";
case LiteBackendActivationReadinessIssue::SdxlApiRequested: return "SdxlApiRequested";
case LiteBackendActivationReadinessIssue::ServerConnectivityCheckRequested: return "ServerConnectivityCheckRequested";
case LiteBackendActivationReadinessIssue::WalletLifecycleRequested: return "WalletLifecycleRequested";
case LiteBackendActivationReadinessIssue::SyncRequested: return "SyncRequested";
case LiteBackendActivationReadinessIssue::SyncStatusPollingRequested: return "SyncStatusPollingRequested";
case LiteBackendActivationReadinessIssue::WorkerQueueRequested: return "WorkerQueueRequested";
case LiteBackendActivationReadinessIssue::WalletStateMutationRequested: return "WalletStateMutationRequested";
case LiteBackendActivationReadinessIssue::WalletPersistenceRequested: return "WalletPersistenceRequested";
case LiteBackendActivationReadinessIssue::UploadRequested: return "UploadRequested";
case LiteBackendActivationReadinessIssue::SigningRequested: return "SigningRequested";
case LiteBackendActivationReadinessIssue::PublicationRequested: return "PublicationRequested";
}
return "Unknown";
}
LiteBackendArtifactResolverResult evaluateLiteBackendArtifactResolver(
const LiteBackendArtifactResolverInput& input,
LiteBackendArtifactResolverOptions options)
{
LiteBackendArtifactResolverResult result;
result = rejectResolverRuntimeAction(std::move(result), input, options);
if (!result.issues.empty()) return result;
if (options.requireResolverOwner && !input.resolverOwnerReady) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::WaitingForResolverOwner,
LiteBackendArtifactResolverIssue::ResolverOwnerMissing,
"lite backend artifact resolver owner is not ready");
}
result.resolverOwnerAccepted = true;
if (options.requireReadOnlyGate && !input.readOnlyGateReady) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::WaitingForReadOnlyGate,
LiteBackendArtifactResolverIssue::ReadOnlyGateMissing,
"lite backend artifact resolver read-only gate is not ready");
}
result.readOnlyGateAccepted = true;
auto candidates = expandSearchRoots(input, result, options);
if (!result.issues.empty()) return result;
for (const auto& candidate : input.candidates) candidates.push_back(candidate);
result.candidateCount = candidates.size();
if (options.requireArtifactCandidates && candidates.empty()) {
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::WaitingForArtifactCandidate,
LiteBackendArtifactResolverIssue::ArtifactCandidateMissing,
"lite backend artifact resolver has no configured candidates");
}
result.artifactCandidatesAccepted = true;
CandidateInspectionResult firstRejected;
bool haveRejectedCandidate = false;
for (const auto& candidate : candidates) {
++result.checkedCandidateCount;
auto inspection = inspectCandidate(input, candidate, options);
if (!inspection.ok) {
++result.rejectedCandidateCount;
if (!haveRejectedCandidate) {
firstRejected = std::move(inspection);
haveRejectedCandidate = true;
}
continue;
}
result.ok = true;
result.status = LiteBackendArtifactResolverStatus::ReadyForActivationReadiness;
result.artifact = std::move(inspection.artifact);
result.syncArtifactInput = std::move(inspection.syncArtifactInput);
result.artifactDiscovered = true;
result.artifactMetadataAccepted = true;
result.platformAccepted = true;
result.versionAccepted = !options.requireVersionMetadata || !result.artifact.versionLabel.empty();
result.provenanceAccepted = !options.requireProvenanceMetadata || provenanceComplete(result.artifact.provenance);
result.sdxlCompatibilityAccepted = !options.requireSdxlCompatibility || result.artifact.sdxlCompatible;
result.symbolMetadataAccepted = !options.requireSdxlSymbols ||
(coreSymbolsReady(result.artifact.symbols) && result.artifact.symbols.freeString && result.artifact.symbols.shutdown);
result.executableMetadataAccepted = true;
result.syncArtifactInputProduced = true;
return result;
}
if (haveRejectedCandidate) {
return stoppedResolverResult(std::move(result), firstRejected.status, firstRejected.issue, firstRejected.message);
}
return stoppedResolverResult(std::move(result), LiteBackendArtifactResolverStatus::WaitingForArtifactCandidate,
LiteBackendArtifactResolverIssue::ArtifactCandidateMissing,
"lite backend artifact resolver did not find any artifact candidates");
}
LiteBackendActivationReadinessResult evaluateLiteBackendActivationReadiness(
const LiteBackendActivationReadinessInput& input,
LiteBackendActivationReadinessOptions options)
{
LiteBackendActivationReadinessResult result;
result.connectionAvailability = LiteConnectionAvailability::BackendUnavailable;
result = rejectActivationRuntimeAction(std::move(result), input, options);
if (!result.issues.empty()) return result;
if (options.requireActivationOwner && !input.activationOwnerReady) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::WaitingForActivationOwner,
LiteBackendActivationReadinessIssue::ActivationOwnerMissing,
"lite backend activation readiness owner is not ready");
}
result.activationOwnerAccepted = true;
if (options.requireReadOnlyGate && !input.readOnlyGateReady) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::WaitingForReadOnlyGate,
LiteBackendActivationReadinessIssue::ReadOnlyGateMissing,
"lite backend activation readiness read-only gate is not ready");
}
result.readOnlyGateAccepted = true;
if (options.requireLiteBuild && !isLiteBuild(input.capabilities)) {
result.connectionAvailability = LiteConnectionAvailability::UnsupportedBuild;
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::WaitingForLiteBuild,
LiteBackendActivationReadinessIssue::FullNodeBuild,
"lite backend activation readiness requires a lite build");
}
result.liteBuildAccepted = true;
if (options.requireLiteBackendCapability && !supportsLiteBackend(input.capabilities)) {
result.connectionAvailability = LiteConnectionAvailability::BackendUnavailable;
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::WaitingForBackendCapability,
LiteBackendActivationReadinessIssue::LiteBackendCapabilityMissing,
"lite backend capability is not available for activation readiness");
}
result.backendCapabilityAccepted = true;
result.artifactResolverResult = evaluateLiteBackendArtifactResolver(input.artifactResolver, options.resolverOptions);
if (options.requireResolvedArtifact && !result.artifactResolverResult.ok) {
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::WaitingForArtifactResolver,
LiteBackendActivationReadinessIssue::ArtifactResolverRejected,
result.artifactResolverResult.error.empty()
? "lite backend artifact resolver did not produce a usable artifact"
: result.artifactResolverResult.error);
}
result.artifactResolverAccepted = true;
result.syncArtifactInput = result.artifactResolverResult.syncArtifactInput;
if (options.requireLinkedBackend && !input.backend.linked) {
result.connectionAvailability = LiteConnectionAvailability::BackendUnavailable;
const auto status = backendStatusOrDefault(input.backend, "lite backend is not linked");
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::WaitingForBackendLink,
LiteBackendActivationReadinessIssue::BackendNotLinked,
status.message);
}
result.backendLinkedAccepted = true;
if (options.requireBridgeAvailable && !input.backend.bridgeAvailable) {
result.connectionAvailability = LiteConnectionAvailability::BridgeUnavailable;
const std::string message = !input.backend.bridgeUnavailableReason.empty()
? input.backend.bridgeUnavailableReason
: backendStatusOrDefault(input.backend, "lite client bridge is unavailable").message;
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::WaitingForBridge,
LiteBackendActivationReadinessIssue::BridgeUnavailable,
message);
}
result.bridgeAccepted = true;
result.selectedServer = selectLiteServer(input.connectionSettings);
if (options.requireUsableServer && !result.selectedServer.ok) {
result.connectionAvailability = LiteConnectionAvailability::NoUsableServer;
return stoppedActivationResult(std::move(result), LiteBackendActivationReadinessStatus::WaitingForConnectionSettings,
LiteBackendActivationReadinessIssue::ConnectionSettingsRejected,
result.selectedServer.error.empty()
? "no usable lite server is configured for activation readiness"
: result.selectedServer.error,
WalletBackendState::Error);
}
result.connectionSettingsAccepted = true;
result.syncBackendInput.linked = input.backend.linked;
result.syncBackendInput.bridgeAvailable = input.backend.bridgeAvailable;
result.syncBackendInput.status = input.backend.status.message.empty()
? WalletBackendStatus{WalletBackendState::Disconnected, "lite backend bridge readiness accepted", {}, {}, 0.0}
: input.backend.status;
result.syncBackendInput.bridgeUnavailableReason = input.backend.bridgeUnavailableReason;
result.syncReadinessInputsProduced = true;
result.ok = true;
result.status = LiteBackendActivationReadinessStatus::ReadyForConnectionReadiness;
result.connectionAvailability = LiteConnectionAvailability::Ready;
result.connectionStatus = readyConnectionStatus(result.selectedServer);
result.connectionServiceBoundaryAccepted = true;
return result;
}
LiteBackendArtifactResolver::LiteBackendArtifactResolver(LiteBackendArtifactResolverOptions options)
: options_(options)
{
}
LiteBackendArtifactResolverResult LiteBackendArtifactResolver::resolve(
const LiteBackendArtifactResolverInput& input) const
{
return evaluateLiteBackendArtifactResolver(input, options_);
}
LiteBackendActivationReadinessAdapter::LiteBackendActivationReadinessAdapter(
LiteBackendActivationReadinessOptions options)
: options_(options)
{
}
LiteBackendActivationReadinessResult LiteBackendActivationReadinessAdapter::evaluate(
const LiteBackendActivationReadinessInput& input) const
{
return evaluateLiteBackendActivationReadiness(input, options_);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,414 @@
#pragma once
#include "lite_connection_service.h"
#include "lite_wallet_sync_execution_readiness.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteBackendArtifactPlatform {
Current,
Linux,
Windows,
MacOS,
Unknown,
};
enum class LiteBackendArtifactKind {
Unknown,
StaticLibrary,
SharedLibrary,
Executable,
};
enum class LiteBackendArtifactResolverStatus {
ReadyForActivationReadiness,
WaitingForResolverOwner,
WaitingForReadOnlyGate,
WaitingForArtifactSearchRoot,
WaitingForArtifactCandidate,
WaitingForArtifactMetadata,
RuntimeActionDisabled,
};
enum class LiteBackendArtifactResolverIssue {
ResolverOwnerMissing,
ReadOnlyGateMissing,
ArtifactSearchRootMissing,
ArtifactSearchRootNotConfigured,
ArtifactSearchRootNotDirectory,
ArtifactCandidateMissing,
ArtifactCandidateNotConfigured,
ArtifactPathMissing,
ArtifactMissing,
ArtifactNotRegularFile,
ArtifactUnreadable,
UnsupportedArtifactKind,
PlatformMismatch,
VersionMissing,
ProvenanceOwnerMissing,
ProvenanceMetadataMissing,
ArtifactNotSdxlCompatible,
ArtifactSymbolsMissing,
ArtifactStringOwnershipUnverified,
ArtifactShutdownUnavailable,
ExecutableMetadataMissing,
ArtifactMutationRequested,
SdxlApiRequested,
ServerConnectivityCheckRequested,
WalletLifecycleRequested,
SyncRequested,
SyncStatusPollingRequested,
WorkerQueueRequested,
WalletStateMutationRequested,
WalletPersistenceRequested,
UploadRequested,
SigningRequested,
PublicationRequested,
};
struct LiteBackendArtifactProvenanceMetadata {
bool ownerReady = false;
bool metadataProvided = false;
std::string source;
std::string builder;
std::string sourceRevision;
std::string artifactSetId;
bool redacted = true;
};
struct LiteBackendArtifactSignatureVerificationMetadata {
bool policyDefined = false;
bool requiredForRelease = false;
bool metadataProvided = false;
bool verificationPerformed = false;
bool verified = false;
std::string signatureFormat;
std::string signaturePath;
std::string signatureFileSha256;
std::string verificationTool;
std::string verificationCommand;
std::string keyFingerprint;
std::string certificateIdentity;
std::string certificateIssuer;
std::string transparencyLogUrl;
std::string verifiedArtifactSha256;
};
struct LiteBackendArtifactCandidate {
bool configured = false;
std::string artifactPath;
LiteBackendArtifactPlatform platform = LiteBackendArtifactPlatform::Current;
LiteBackendArtifactKind kind = LiteBackendArtifactKind::Unknown;
std::string versionLabel;
LiteBackendArtifactProvenanceMetadata provenance;
LiteBackendArtifactSignatureVerificationMetadata signatureVerification;
bool sdxlCompatible = false;
LiteWalletSdxlArtifactSymbolsInput symbols;
bool executableMetadataRequired = false;
};
struct LiteBackendArtifactSearchRoot {
bool configured = false;
std::string rootPath;
LiteBackendArtifactPlatform platform = LiteBackendArtifactPlatform::Current;
LiteBackendArtifactKind kind = LiteBackendArtifactKind::Unknown;
std::string versionLabel;
LiteBackendArtifactProvenanceMetadata provenance;
LiteBackendArtifactSignatureVerificationMetadata signatureVerification;
bool sdxlCompatible = false;
LiteWalletSdxlArtifactSymbolsInput symbols;
bool executableMetadataRequired = false;
std::vector<std::string> expectedArtifactNames;
};
struct LiteBackendArtifactResolverInput {
bool resolverOwnerReady = false;
bool readOnlyGateReady = false;
std::string projectRoot;
LiteBackendArtifactPlatform expectedPlatform = LiteBackendArtifactPlatform::Current;
std::vector<LiteBackendArtifactSearchRoot> searchRoots;
std::vector<LiteBackendArtifactCandidate> candidates;
bool artifactMutationRequested = false;
bool sdxlApiRequested = false;
bool serverConnectivityCheckRequested = false;
bool walletLifecycleRequested = false;
bool syncRequested = false;
bool syncStatusPollingRequested = false;
bool workerQueueRequested = false;
bool walletStateMutationRequested = false;
bool walletPersistenceRequested = false;
bool uploadRequested = false;
bool signingRequested = false;
bool publicationRequested = false;
};
struct LiteBackendArtifactResolverOptions {
bool requireResolverOwner = true;
bool requireReadOnlyGate = true;
bool requireArtifactCandidates = true;
bool requireVersionMetadata = true;
bool requireProvenanceMetadata = true;
bool requireSdxlCompatibility = true;
bool requireSdxlSymbols = true;
bool rejectArtifactMutation = true;
bool rejectSdxlApiCalls = true;
bool rejectServerConnectivityChecks = true;
bool rejectWalletLifecycle = true;
bool rejectSync = true;
bool rejectSyncStatusPolling = true;
bool rejectWorkerQueue = true;
bool rejectWalletStateMutation = true;
bool rejectWalletPersistence = true;
bool rejectUpload = true;
bool rejectSigning = true;
bool rejectPublication = true;
};
struct LiteBackendResolvedArtifact {
bool found = false;
std::string artifactPath;
LiteBackendArtifactPlatform platform = LiteBackendArtifactPlatform::Unknown;
LiteBackendArtifactKind kind = LiteBackendArtifactKind::Unknown;
std::string versionLabel;
LiteBackendArtifactProvenanceMetadata provenance;
LiteBackendArtifactSignatureVerificationMetadata signatureVerification;
bool exists = false;
bool regularFile = false;
bool readable = false;
bool executable = false;
bool sdxlCompatible = false;
LiteWalletSdxlArtifactSymbolsInput symbols;
std::size_t artifactSizeBytes = 0;
};
struct LiteBackendArtifactResolverIssueInfo {
LiteBackendArtifactResolverIssue issue = LiteBackendArtifactResolverIssue::ArtifactCandidateMissing;
std::string message;
};
struct LiteBackendArtifactResolverResult {
bool ok = false;
bool readOnlyArtifactDiscovery = true;
bool artifactMetadataReadOnly = true;
bool provenanceReadOnly = true;
bool executableMetadataReadOnly = true;
bool noArtifactMutation = true;
bool noSdxlCalls = true;
bool noServerConnectivityChecked = true;
bool noWalletCreated = true;
bool noWalletOpened = true;
bool noWalletRestored = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWorkerQueueEnqueue = true;
bool noWalletStateMutation = true;
bool noWalletPersistence = true;
bool noUpload = true;
bool noSigning = true;
bool noPublication = true;
bool resolverOwnerAccepted = false;
bool readOnlyGateAccepted = false;
bool artifactSearchRootsAccepted = false;
bool artifactCandidatesAccepted = false;
bool artifactDiscovered = false;
bool artifactMetadataAccepted = false;
bool platformAccepted = false;
bool versionAccepted = false;
bool provenanceAccepted = false;
bool sdxlCompatibilityAccepted = false;
bool symbolMetadataAccepted = false;
bool executableMetadataAccepted = false;
bool syncArtifactInputProduced = false;
std::size_t searchRootCount = 0;
std::size_t candidateCount = 0;
std::size_t checkedCandidateCount = 0;
std::size_t rejectedCandidateCount = 0;
LiteBackendArtifactResolverStatus status = LiteBackendArtifactResolverStatus::WaitingForResolverOwner;
LiteBackendResolvedArtifact artifact;
LiteWalletSdxlArtifactInput syncArtifactInput;
std::vector<LiteBackendArtifactResolverIssueInfo> issues;
std::string error;
};
enum class LiteBackendActivationReadinessStatus {
ReadyForConnectionReadiness,
WaitingForActivationOwner,
WaitingForReadOnlyGate,
WaitingForLiteBuild,
WaitingForBackendCapability,
WaitingForArtifactResolver,
WaitingForBackendLink,
WaitingForBridge,
WaitingForConnectionSettings,
RuntimeActionDisabled,
};
enum class LiteBackendActivationReadinessIssue {
ActivationOwnerMissing,
ReadOnlyGateMissing,
FullNodeBuild,
LiteBackendCapabilityMissing,
ArtifactResolverRejected,
BackendNotLinked,
BridgeUnavailable,
ConnectionSettingsRejected,
ArtifactMutationRequested,
SdxlApiRequested,
ServerConnectivityCheckRequested,
WalletLifecycleRequested,
SyncRequested,
SyncStatusPollingRequested,
WorkerQueueRequested,
WalletStateMutationRequested,
WalletPersistenceRequested,
UploadRequested,
SigningRequested,
PublicationRequested,
};
struct LiteBackendBridgeReadinessInput {
bool linked = false;
bool bridgeAvailable = false;
WalletBackendStatus status;
std::string bridgeUnavailableReason;
};
struct LiteBackendActivationReadinessInput {
WalletCapabilities capabilities;
LiteConnectionSettings connectionSettings;
LiteBackendArtifactResolverInput artifactResolver;
LiteBackendBridgeReadinessInput backend;
bool activationOwnerReady = false;
bool readOnlyGateReady = false;
bool artifactMutationRequested = false;
bool sdxlApiRequested = false;
bool serverConnectivityCheckRequested = false;
bool walletLifecycleRequested = false;
bool syncRequested = false;
bool syncStatusPollingRequested = false;
bool workerQueueRequested = false;
bool walletStateMutationRequested = false;
bool walletPersistenceRequested = false;
bool uploadRequested = false;
bool signingRequested = false;
bool publicationRequested = false;
};
struct LiteBackendActivationReadinessOptions {
bool requireActivationOwner = true;
bool requireReadOnlyGate = true;
bool requireLiteBuild = true;
bool requireLiteBackendCapability = true;
bool requireResolvedArtifact = true;
bool requireLinkedBackend = true;
bool requireBridgeAvailable = true;
bool requireUsableServer = true;
bool rejectArtifactMutation = true;
bool rejectSdxlApiCalls = true;
bool rejectServerConnectivityChecks = true;
bool rejectWalletLifecycle = true;
bool rejectSync = true;
bool rejectSyncStatusPolling = true;
bool rejectWorkerQueue = true;
bool rejectWalletStateMutation = true;
bool rejectWalletPersistence = true;
bool rejectUpload = true;
bool rejectSigning = true;
bool rejectPublication = true;
LiteBackendArtifactResolverOptions resolverOptions;
};
struct LiteBackendActivationReadinessIssueInfo {
LiteBackendActivationReadinessIssue issue = LiteBackendActivationReadinessIssue::ActivationOwnerMissing;
std::string message;
};
struct LiteBackendActivationReadinessResult {
bool ok = false;
bool readOnlyActivation = true;
bool artifactDiscoveryReadOnly = true;
bool connectionReadinessOnly = true;
bool noBridgeCalls = true;
bool noArtifactMutation = true;
bool noSdxlCalls = true;
bool noServerConnectivityChecked = true;
bool noWalletCreated = true;
bool noWalletOpened = true;
bool noWalletRestored = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWorkerQueueEnqueue = true;
bool noWalletStateMutation = true;
bool noWalletPersistence = true;
bool noUpload = true;
bool noSigning = true;
bool noPublication = true;
bool activationOwnerAccepted = false;
bool readOnlyGateAccepted = false;
bool liteBuildAccepted = false;
bool backendCapabilityAccepted = false;
bool artifactResolverAccepted = false;
bool backendLinkedAccepted = false;
bool bridgeAccepted = false;
bool connectionSettingsAccepted = false;
bool connectionServiceBoundaryAccepted = false;
bool syncReadinessInputsProduced = false;
LiteBackendActivationReadinessStatus status = LiteBackendActivationReadinessStatus::WaitingForActivationOwner;
LiteBackendArtifactResolverResult artifactResolverResult;
LiteWalletSdxlArtifactInput syncArtifactInput;
LiteWalletLinkedBackendReadinessInput syncBackendInput;
LiteConnectionAvailability connectionAvailability = LiteConnectionAvailability::BackendUnavailable;
LiteServerSelectionResult selectedServer;
WalletBackendStatus connectionStatus;
std::vector<LiteBackendActivationReadinessIssueInfo> issues;
std::string error;
};
LiteBackendArtifactPlatform currentLiteBackendArtifactPlatform();
const char* liteBackendArtifactPlatformName(LiteBackendArtifactPlatform platform);
const char* liteBackendArtifactKindName(LiteBackendArtifactKind kind);
const char* liteBackendArtifactResolverStatusName(LiteBackendArtifactResolverStatus status);
const char* liteBackendArtifactResolverIssueName(LiteBackendArtifactResolverIssue issue);
const char* liteBackendActivationReadinessStatusName(LiteBackendActivationReadinessStatus status);
const char* liteBackendActivationReadinessIssueName(LiteBackendActivationReadinessIssue issue);
LiteBackendArtifactResolverResult evaluateLiteBackendArtifactResolver(
const LiteBackendArtifactResolverInput& input,
LiteBackendArtifactResolverOptions options = {});
LiteBackendActivationReadinessResult evaluateLiteBackendActivationReadiness(
const LiteBackendActivationReadinessInput& input,
LiteBackendActivationReadinessOptions options = {});
class LiteBackendArtifactResolver {
public:
explicit LiteBackendArtifactResolver(LiteBackendArtifactResolverOptions options = {});
LiteBackendArtifactResolverResult resolve(const LiteBackendArtifactResolverInput& input) const;
private:
LiteBackendArtifactResolverOptions options_;
};
class LiteBackendActivationReadinessAdapter {
public:
explicit LiteBackendActivationReadinessAdapter(LiteBackendActivationReadinessOptions options = {});
LiteBackendActivationReadinessResult evaluate(const LiteBackendActivationReadinessInput& input) const;
private:
LiteBackendActivationReadinessOptions options_;
};
} // namespace dragonx::wallet

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,186 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "lite_client_bridge.h"
#include "lite_bridge_runtime.h"
#ifndef DRAGONX_ENABLE_LITE_BACKEND
#define DRAGONX_ENABLE_LITE_BACKEND 0
#endif
#if DRAGONX_ENABLE_LITE_BACKEND
extern "C" {
bool litelib_wallet_exists(const char* chain_name);
char* litelib_initialize_new(bool dangerous, const char* server);
char* litelib_initialize_new_from_phrase(bool dangerous,
const char* server,
const char* seed,
unsigned long long birthday,
unsigned long long number,
bool overwrite);
char* litelib_initialize_existing(bool dangerous, const char* server);
char* litelib_execute(const char* command, const char* args);
void litelib_rust_free_string(char* value);
bool litelib_check_server_online(const char* server);
void litelib_shutdown();
}
#endif
#include <utility>
namespace dragonx {
namespace wallet {
namespace {
bool hasRequiredApi(const LiteClientBridgeApi& api)
{
return api.walletExists &&
api.initializeNew &&
api.initializeNewFromPhrase &&
api.initializeExisting &&
api.execute &&
api.freeString &&
api.checkServerOnline &&
api.shutdown;
}
#if DRAGONX_ENABLE_LITE_BACKEND
LiteClientBridgeApi linkedSdxlApi()
{
return LiteClientBridgeApi{
&litelib_wallet_exists,
&litelib_initialize_new,
&litelib_initialize_new_from_phrase,
&litelib_initialize_existing,
&litelib_execute,
&litelib_rust_free_string,
&litelib_check_server_online,
&litelib_shutdown
};
}
#endif
} // namespace
LiteClientBridge::LiteClientBridge(LiteClientBridgeApi api, std::string unavailableReason)
: api_(api), unavailableReason_(std::move(unavailableReason))
{
}
LiteClientBridge LiteClientBridge::unavailable(std::string reason)
{
return LiteClientBridge({}, std::move(reason));
}
LiteClientBridge LiteClientBridge::fromApi(LiteClientBridgeApi api)
{
if (!hasRequiredApi(api)) {
return unavailable("lite client bridge API is incomplete");
}
return LiteClientBridge(api, {});
}
LiteClientBridge LiteClientBridge::linkedSdxl()
{
#if DRAGONX_ENABLE_LITE_BACKEND
return fromApi(linkedSdxlApi());
#else
return unavailable("lite backend is not linked");
#endif
}
LiteClientBridge::LiteClientBridge(LiteClientBridge&& other) noexcept
: api_(other.api_),
unavailableReason_(std::move(other.unavailableReason_)),
shutdownCalled_(other.shutdownCalled_)
{
other.api_ = {};
other.shutdownCalled_ = true;
}
LiteClientBridge& LiteClientBridge::operator=(LiteClientBridge&& other) noexcept
{
if (this == &other) return *this;
shutdown();
api_ = other.api_;
unavailableReason_ = std::move(other.unavailableReason_);
shutdownCalled_ = other.shutdownCalled_;
other.api_ = {};
other.shutdownCalled_ = true;
return *this;
}
LiteClientBridge::~LiteClientBridge()
{
shutdown();
}
bool LiteClientBridge::available() const
{
return hasRequiredApi(api_);
}
bool LiteClientBridge::walletExists(const std::string& chainName) const
{
if (!available()) return false;
return api_.walletExists(chainName.c_str());
}
bool LiteClientBridge::checkServerOnline(const std::string& server) const
{
if (!available()) return false;
return api_.checkServerOnline(server.c_str());
}
LiteBridgeStringResult LiteClientBridge::initializeNew(bool dangerous, const std::string& server)
{
if (!available()) return unavailableResult();
return takeOwnedString(api_.initializeNew(dangerous, server.c_str()));
}
LiteBridgeStringResult LiteClientBridge::initializeNewFromPhrase(bool dangerous,
const std::string& server,
const std::string& seed,
unsigned long long birthday,
unsigned long long account,
bool overwrite)
{
if (!available()) return unavailableResult();
return takeOwnedString(api_.initializeNewFromPhrase(
dangerous, server.c_str(), seed.c_str(), birthday, account, overwrite));
}
LiteBridgeStringResult LiteClientBridge::initializeExisting(bool dangerous, const std::string& server)
{
if (!available()) return unavailableResult();
return takeOwnedString(api_.initializeExisting(dangerous, server.c_str()));
}
LiteBridgeStringResult LiteClientBridge::execute(const std::string& command, const std::string& args)
{
if (!available()) return unavailableResult();
if (command.empty()) return {false, {}, "lite command is empty"};
return takeOwnedString(api_.execute(command.c_str(), args.c_str()));
}
void LiteClientBridge::shutdown()
{
if (shutdownCalled_) return;
shutdownCalled_ = true;
if (available()) api_.shutdown();
}
LiteBridgeStringResult LiteClientBridge::unavailableResult() const
{
return {false, {}, unavailableReason_.empty() ? "lite backend is unavailable" : unavailableReason_};
}
LiteBridgeStringResult LiteClientBridge::takeOwnedString(char* rawValue) const
{
return liteBridgeRuntimeTakeOwnedString(rawValue, api_.freeString);
}
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,84 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
namespace wallet {
struct LiteBridgeStringResult {
bool ok = false;
std::string value;
std::string error;
};
struct LiteClientBridgeApi {
using WalletExistsFn = bool (*)(const char* chainName);
using InitializeNewFn = char* (*)(bool dangerous, const char* server);
using InitializeNewFromPhraseFn = char* (*)(bool dangerous,
const char* server,
const char* seed,
unsigned long long birthday,
unsigned long long account,
bool overwrite);
using InitializeExistingFn = char* (*)(bool dangerous, const char* server);
using ExecuteFn = char* (*)(const char* command, const char* args);
using FreeStringFn = void (*)(char* value);
using CheckServerOnlineFn = bool (*)(const char* server);
using ShutdownFn = void (*)();
WalletExistsFn walletExists = nullptr;
InitializeNewFn initializeNew = nullptr;
InitializeNewFromPhraseFn initializeNewFromPhrase = nullptr;
InitializeExistingFn initializeExisting = nullptr;
ExecuteFn execute = nullptr;
FreeStringFn freeString = nullptr;
CheckServerOnlineFn checkServerOnline = nullptr;
ShutdownFn shutdown = nullptr;
};
class LiteClientBridge {
public:
static LiteClientBridge unavailable(std::string reason);
static LiteClientBridge fromApi(LiteClientBridgeApi api);
static LiteClientBridge linkedSdxl();
LiteClientBridge(const LiteClientBridge&) = delete;
LiteClientBridge& operator=(const LiteClientBridge&) = delete;
LiteClientBridge(LiteClientBridge&& other) noexcept;
LiteClientBridge& operator=(LiteClientBridge&& other) noexcept;
~LiteClientBridge();
bool available() const;
const std::string& unavailableReason() const { return unavailableReason_; }
bool walletExists(const std::string& chainName) const;
bool checkServerOnline(const std::string& server) const;
LiteBridgeStringResult initializeNew(bool dangerous, const std::string& server);
LiteBridgeStringResult initializeNewFromPhrase(bool dangerous,
const std::string& server,
const std::string& seed,
unsigned long long birthday,
unsigned long long account,
bool overwrite);
LiteBridgeStringResult initializeExisting(bool dangerous, const std::string& server);
LiteBridgeStringResult execute(const std::string& command, const std::string& args);
void shutdown();
private:
LiteClientBridge(LiteClientBridgeApi api, std::string unavailableReason);
LiteBridgeStringResult unavailableResult() const;
LiteBridgeStringResult takeOwnedString(char* rawValue) const;
LiteClientBridgeApi api_;
std::string unavailableReason_;
bool shutdownCalled_ = false;
};
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,309 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "lite_connection_service.h"
#include <algorithm>
#include <cctype>
#include <utility>
namespace dragonx {
namespace wallet {
namespace {
std::string trimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
bool startsWith(const std::string& value, const char* prefix)
{
const std::string prefixValue(prefix);
return value.size() >= prefixValue.size() &&
value.compare(0, prefixValue.size(), prefixValue) == 0;
}
LiteServerEndpoint trimmedEndpoint(const LiteServerEndpoint& endpoint)
{
LiteServerEndpoint normalized = endpoint;
normalized.url = trimCopy(normalized.url);
return normalized;
}
std::vector<std::pair<std::size_t, LiteServerEndpoint>> usableConfiguredServers(
const LiteConnectionSettings& settings)
{
std::vector<std::pair<std::size_t, LiteServerEndpoint>> servers;
for (std::size_t index = 0; index < settings.servers.size(); ++index) {
auto endpoint = trimmedEndpoint(settings.servers[index]);
if (!endpoint.enabled || !isLiteServerUrlUsable(endpoint.url)) continue;
servers.push_back({index, std::move(endpoint)});
}
return servers;
}
std::string normalizedChainName(const std::string& chainName)
{
const std::string normalized = trimCopy(chainName);
return normalized.empty() ? kDragonXLiteChainName : normalized;
}
} // namespace
std::vector<LiteServerEndpoint> defaultDragonXLiteServers()
{
return {
{"https://lite.dragonx.is", "DragonX Lite", true},
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
};
}
LiteConnectionSettings defaultLiteConnectionSettings()
{
LiteConnectionSettings settings;
settings.servers = defaultDragonXLiteServers();
settings.selectionMode = LiteServerSelectionMode::Sticky;
settings.stickyServerUrl = kDefaultDragonXLiteServer;
settings.chainName = kDragonXLiteChainName;
settings.randomSelectionSeed = 0;
return settings;
}
bool isLiteServerUrlUsable(const std::string& serverUrl)
{
const std::string normalized = trimCopy(serverUrl);
return startsWith(normalized, "https://") || startsWith(normalized, "http://");
}
const char* liteServerSelectionModeName(LiteServerSelectionMode mode)
{
switch (mode) {
case LiteServerSelectionMode::Sticky:
return "Sticky";
case LiteServerSelectionMode::Random:
return "Random";
}
return "Unknown";
}
const char* liteConnectionAvailabilityName(LiteConnectionAvailability availability)
{
switch (availability) {
case LiteConnectionAvailability::Ready:
return "Ready";
case LiteConnectionAvailability::UnsupportedBuild:
return "UnsupportedBuild";
case LiteConnectionAvailability::BackendUnavailable:
return "BackendUnavailable";
case LiteConnectionAvailability::BridgeUnavailable:
return "BridgeUnavailable";
case LiteConnectionAvailability::NoUsableServer:
return "NoUsableServer";
}
return "Unknown";
}
LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings)
{
auto usableServers = usableConfiguredServers(settings);
const std::string stickyServerUrl = trimCopy(settings.stickyServerUrl);
if (settings.selectionMode == LiteServerSelectionMode::Sticky &&
isLiteServerUrlUsable(stickyServerUrl)) {
auto match = std::find_if(usableServers.begin(), usableServers.end(), [&](const auto& candidate) {
return candidate.second.url == stickyServerUrl;
});
if (match != usableServers.end()) {
return LiteServerSelectionResult{true, match->second, match->first, false, {}};
}
return LiteServerSelectionResult{
true,
LiteServerEndpoint{stickyServerUrl, "Custom", true},
0,
true,
{}
};
}
if (usableServers.empty()) {
return LiteServerSelectionResult{false, {}, 0, false, "no usable lite servers are configured"};
}
if (settings.selectionMode == LiteServerSelectionMode::Random) {
const std::size_t selectedIndex = settings.randomSelectionSeed % usableServers.size();
return LiteServerSelectionResult{
true,
usableServers[selectedIndex].second,
usableServers[selectedIndex].first,
false,
{}
};
}
return LiteServerSelectionResult{true, usableServers.front().second, usableServers.front().first, false, {}};
}
LiteConnectionService::LiteConnectionService(WalletCapabilities capabilities,
LiteConnectionSettings settings,
LiteClientBridge bridge)
: capabilities_(capabilities), settings_(std::move(settings)), bridge_(std::move(bridge))
{
}
LiteConnectionAvailability LiteConnectionService::availability() const
{
if (!isLiteBuild(capabilities_)) return LiteConnectionAvailability::UnsupportedBuild;
if (!supportsLiteBackend(capabilities_)) return LiteConnectionAvailability::BackendUnavailable;
if (!bridge_.available()) return LiteConnectionAvailability::BridgeUnavailable;
if (!selectedServer().ok) return LiteConnectionAvailability::NoUsableServer;
return LiteConnectionAvailability::Ready;
}
WalletBackendStatus LiteConnectionService::status() const
{
const auto currentAvailability = availability();
if (currentAvailability == LiteConnectionAvailability::NoUsableServer) {
return statusFor(currentAvailability, selectedServer().error);
}
return statusFor(currentAvailability);
}
LiteServerSelectionResult LiteConnectionService::selectedServer() const
{
return selectLiteServer(settings_);
}
LiteWalletExistsRequest LiteConnectionService::walletExistsRequest() const
{
return LiteWalletExistsRequest{normalizedChainName(settings_.chainName)};
}
LiteServerHealthRequest LiteConnectionService::serverHealthRequest() const
{
auto selection = selectedServer();
if (!selection.ok) return {};
return LiteServerHealthRequest{selection.server, selection.serverIndex, selection.customServer};
}
LiteWalletExistsCheckResult LiteConnectionService::checkWalletExists()
{
LiteWalletExistsCheckResult result;
result.request = walletExistsRequest();
const auto readiness = bridgeReadinessStatus();
if (readiness.state != WalletBackendState::Disconnected) {
result.status = readiness;
result.error = readiness.message;
return result;
}
if (result.request.chainName.empty()) {
result.status = statusFor(LiteConnectionAvailability::BackendUnavailable, "lite chain name is empty");
result.error = result.status.message;
return result;
}
result.attempted = true;
result.walletExists = bridge_.walletExists(result.request.chainName);
result.ok = true;
result.status = readiness;
return result;
}
LiteServerHealthCheckResult LiteConnectionService::checkSelectedServerHealth()
{
LiteServerHealthCheckResult result;
result.request = serverHealthRequest();
const auto readiness = bridgeReadinessStatus();
if (readiness.state != WalletBackendState::Disconnected) {
result.status = readiness;
result.error = readiness.message;
return result;
}
auto selection = selectedServer();
if (!selection.ok) {
result.status = statusFor(LiteConnectionAvailability::NoUsableServer, selection.error);
result.error = result.status.message;
return result;
}
result.request = LiteServerHealthRequest{selection.server, selection.serverIndex, selection.customServer};
result.attempted = true;
result.serverOnline = bridge_.checkServerOnline(result.request.server.url);
result.ok = true;
result.status = readiness;
return result;
}
WalletBackendStatus LiteConnectionService::statusFor(LiteConnectionAvailability availability,
const std::string& detail) const
{
switch (availability) {
case LiteConnectionAvailability::Ready:
return WalletBackendStatus{
WalletBackendState::Disconnected,
detail.empty() ? "lite connection scaffold ready; wallet lifecycle is not implemented" : detail,
{},
{},
0.0
};
case LiteConnectionAvailability::UnsupportedBuild:
return WalletBackendStatus{
WalletBackendState::Unavailable,
"lite connection service is unsupported in full-node builds",
{},
{},
0.0
};
case LiteConnectionAvailability::BackendUnavailable:
return WalletBackendStatus{
WalletBackendState::Unavailable,
detail.empty() ? "lite backend is not linked" : detail,
{},
{},
0.0
};
case LiteConnectionAvailability::BridgeUnavailable:
return WalletBackendStatus{
WalletBackendState::Unavailable,
detail.empty() ? bridge_.unavailableReason() : detail,
{},
{},
0.0
};
case LiteConnectionAvailability::NoUsableServer:
return WalletBackendStatus{
WalletBackendState::Error,
detail.empty() ? "no usable lite servers are configured" : detail,
{},
{},
0.0
};
}
return WalletBackendStatus{WalletBackendState::Unavailable, "unknown lite connection state", {}, {}, 0.0};
}
WalletBackendStatus LiteConnectionService::bridgeReadinessStatus() const
{
if (!isLiteBuild(capabilities_)) return statusFor(LiteConnectionAvailability::UnsupportedBuild);
if (!supportsLiteBackend(capabilities_)) return statusFor(LiteConnectionAvailability::BackendUnavailable);
if (!bridge_.available()) return statusFor(LiteConnectionAvailability::BridgeUnavailable);
return statusFor(LiteConnectionAvailability::Ready);
}
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,123 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "lite_client_bridge.h"
#include "wallet_backend.h"
#include "wallet_capabilities.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx {
namespace wallet {
// The SDXL litelib backend identifies the production chain as "main" (its mainnet
// module carries DragonX's coin params). It hard-panics on any other chain name, so
// this MUST be one of {"main","test","regtest"} — not the coin ticker.
constexpr const char* kDragonXLiteChainName = "main";
constexpr const char* kDefaultDragonXLiteServer = "https://lite.dragonx.is";
enum class LiteServerSelectionMode {
Sticky,
Random
};
enum class LiteConnectionAvailability {
Ready,
UnsupportedBuild,
BackendUnavailable,
BridgeUnavailable,
NoUsableServer
};
struct LiteServerEndpoint {
std::string url;
std::string label;
bool enabled = true;
};
struct LiteConnectionSettings {
std::vector<LiteServerEndpoint> servers;
LiteServerSelectionMode selectionMode = LiteServerSelectionMode::Sticky;
std::string stickyServerUrl = kDefaultDragonXLiteServer;
std::string chainName = kDragonXLiteChainName;
std::size_t randomSelectionSeed = 0;
};
struct LiteServerSelectionResult {
bool ok = false;
LiteServerEndpoint server;
std::size_t serverIndex = 0;
bool customServer = false;
std::string error;
};
struct LiteWalletExistsRequest {
std::string chainName = kDragonXLiteChainName;
};
struct LiteServerHealthRequest {
LiteServerEndpoint server;
std::size_t serverIndex = 0;
bool customServer = false;
};
struct LiteWalletExistsCheckResult {
bool ok = false;
bool attempted = false;
bool walletExists = false;
LiteWalletExistsRequest request;
WalletBackendStatus status;
std::string error;
};
struct LiteServerHealthCheckResult {
bool ok = false;
bool attempted = false;
bool serverOnline = false;
LiteServerHealthRequest request;
WalletBackendStatus status;
std::string error;
};
std::vector<LiteServerEndpoint> defaultDragonXLiteServers();
LiteConnectionSettings defaultLiteConnectionSettings();
bool isLiteServerUrlUsable(const std::string& serverUrl);
const char* liteServerSelectionModeName(LiteServerSelectionMode mode);
const char* liteConnectionAvailabilityName(LiteConnectionAvailability availability);
LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings);
class LiteConnectionService {
public:
LiteConnectionService(WalletCapabilities capabilities,
LiteConnectionSettings settings,
LiteClientBridge bridge);
const LiteConnectionSettings& settings() const { return settings_; }
const WalletCapabilities& capabilities() const { return capabilities_; }
LiteConnectionAvailability availability() const;
WalletBackendStatus status() const;
LiteServerSelectionResult selectedServer() const;
LiteWalletExistsRequest walletExistsRequest() const;
LiteServerHealthRequest serverHealthRequest() const;
LiteWalletExistsCheckResult checkWalletExists();
LiteServerHealthCheckResult checkSelectedServerHealth();
private:
WalletBackendStatus statusFor(LiteConnectionAvailability availability,
const std::string& detail = {}) const;
WalletBackendStatus bridgeReadinessStatus() const;
WalletCapabilities capabilities_;
LiteConnectionSettings settings_;
LiteClientBridge bridge_;
};
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,570 @@
#include "wallet/lite_result_parsers.h"
#include <algorithm>
#include <limits>
#include <stdexcept>
namespace dragonx::wallet {
namespace {
using json = nlohmann::json;
std::string fieldPath(const std::string& key)
{
return "$.'" + key + "'";
}
std::string indexPath(const std::string& parentPath, std::size_t index)
{
return parentPath + "[" + std::to_string(index) + "]";
}
template <typename Result>
Result makeResult(LiteResultCommand command)
{
Result result;
result.command = command;
return result;
}
template <typename Result>
bool fail(Result& result,
LiteResultParserError error,
const std::string& path,
const std::string& message)
{
result.ok = false;
result.error = error;
result.errorPath = path;
result.errorMessage = message;
result.issues.push_back({path, message});
return false;
}
template <typename Result>
void succeed(Result& result)
{
result.ok = true;
result.error = LiteResultParserError::None;
result.errorPath.clear();
result.errorMessage.clear();
}
template <typename Result>
bool parseJsonText(const std::string& jsonText, Result& result, json& parsed)
{
parsed = json::parse(jsonText, nullptr, false);
if (parsed.is_discarded()) {
return fail(result, LiteResultParserError::InvalidJson, "$", "response is not valid JSON");
}
return true;
}
template <typename Result>
bool requireObject(const json& value, const std::string& path, Result& result)
{
if (!value.is_object()) {
return fail(result, LiteResultParserError::ExpectedObject, path, "expected a JSON object");
}
return true;
}
template <typename Result>
bool requireArray(const json& value, const std::string& path, Result& result)
{
if (!value.is_array()) {
return fail(result, LiteResultParserError::ExpectedArray, path, "expected a JSON array");
}
return true;
}
bool jsonToUnsigned(const json& value, std::uint64_t& output)
{
try {
if (value.is_number_unsigned()) {
output = value.get<std::uint64_t>();
return true;
}
if (value.is_number_integer()) {
const auto signedValue = value.get<std::int64_t>();
if (signedValue < 0) return false;
output = static_cast<std::uint64_t>(signedValue);
return true;
}
if (value.is_string()) {
const auto text = value.get<std::string>();
if (text.empty()) return false;
std::size_t parsedSize = 0;
const auto parsed = std::stoull(text, &parsedSize, 10);
if (parsedSize != text.size()) return false;
output = static_cast<std::uint64_t>(parsed);
return true;
}
} catch (const std::exception&) {
return false;
}
return false;
}
bool jsonToSigned(const json& value, std::int64_t& output)
{
try {
if (value.is_number_integer()) {
output = value.get<std::int64_t>();
return true;
}
if (value.is_number_unsigned()) {
const auto unsignedValue = value.get<std::uint64_t>();
if (unsignedValue > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
return false;
}
output = static_cast<std::int64_t>(unsignedValue);
return true;
}
if (value.is_string()) {
const auto text = value.get<std::string>();
if (text.empty()) return false;
std::size_t parsedSize = 0;
const auto parsed = std::stoll(text, &parsedSize, 10);
if (parsedSize != text.size()) return false;
output = static_cast<std::int64_t>(parsed);
return true;
}
} catch (const std::exception&) {
return false;
}
return false;
}
template <typename Result>
bool readRequiredStringField(const json& object,
const std::string& key,
std::string& output,
Result& result)
{
if (!object.contains(key) || object.at(key).is_null()) {
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required string field");
}
if (!object.at(key).is_string()) {
return fail(result, LiteResultParserError::InvalidFieldType, fieldPath(key), "expected a string");
}
output = object.at(key).get<std::string>();
return true;
}
template <typename Result>
bool readOptionalStringField(const json& object,
const std::string& key,
std::optional<std::string>& output,
Result& result)
{
if (!object.contains(key) || object.at(key).is_null()) return true;
if (!object.at(key).is_string()) {
return fail(result, LiteResultParserError::InvalidFieldType, fieldPath(key), "expected a string");
}
output = object.at(key).get<std::string>();
return true;
}
template <typename Result>
bool readOptionalStringField(const json& object,
const std::string& key,
std::string& output,
Result& result)
{
if (!object.contains(key) || object.at(key).is_null()) return true;
if (!object.at(key).is_string()) {
return fail(result, LiteResultParserError::InvalidFieldType, fieldPath(key), "expected a string");
}
output = object.at(key).get<std::string>();
return true;
}
template <typename Result>
bool readRequiredSignedField(const json& object,
const std::string& key,
std::int64_t& output,
Result& result)
{
if (!object.contains(key) || object.at(key).is_null()) {
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required integer field");
}
if (!jsonToSigned(object.at(key), output)) {
return fail(result, LiteResultParserError::InvalidFieldValue, fieldPath(key), "expected a signed integer-compatible value");
}
return true;
}
template <typename Result>
bool readRequiredUnsignedField(const json& object,
const std::string& key,
std::uint64_t& output,
Result& result)
{
if (!object.contains(key) || object.at(key).is_null()) {
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required unsigned integer field");
}
if (!jsonToUnsigned(object.at(key), output)) {
return fail(result, LiteResultParserError::InvalidFieldValue, fieldPath(key), "expected an unsigned integer-compatible value");
}
return true;
}
template <typename Result>
bool readOptionalSignedField(const json& object,
const std::string& key,
std::optional<std::int64_t>& output,
Result& result)
{
if (!object.contains(key) || object.at(key).is_null()) return true;
std::int64_t parsed = 0;
if (!jsonToSigned(object.at(key), parsed)) {
return fail(result, LiteResultParserError::InvalidFieldValue, fieldPath(key), "expected a signed integer-compatible value");
}
output = parsed;
return true;
}
template <typename Result>
bool readOptionalBoolField(const json& object,
const std::string& key,
bool& output,
Result& result)
{
if (!object.contains(key) || object.at(key).is_null()) return true;
if (!object.at(key).is_boolean()) {
return fail(result, LiteResultParserError::InvalidFieldType, fieldPath(key), "expected a boolean");
}
output = object.at(key).get<bool>();
return true;
}
template <typename Result>
bool readStringArrayField(const json& object,
const std::string& key,
std::vector<std::string>& output,
Result& result)
{
if (!object.contains(key) || object.at(key).is_null()) {
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required array field");
}
const auto& values = object.at(key);
if (!requireArray(values, fieldPath(key), result)) return false;
output.clear();
for (std::size_t index = 0; index < values.size(); ++index) {
if (!values[index].is_string()) {
return fail(result, LiteResultParserError::InvalidFieldType, indexPath(fieldPath(key), index), "expected a string array item");
}
output.push_back(values[index].get<std::string>());
}
return true;
}
template <typename Result>
bool parseSpendableOutput(const json& value,
LiteSpendableOutputKind kind,
const std::string& path,
LiteSpendableOutput& output,
Result& result)
{
if (!requireObject(value, path, result)) return false;
output.kind = kind;
output.pending = kind == LiteSpendableOutputKind::PendingNote || kind == LiteSpendableOutputKind::PendingUtxo;
if (!readRequiredStringField(value, "address", output.address, result)) return false;
if (!readRequiredStringField(value, "created_in_txid", output.createdInTxid, result)) return false;
if (!readOptionalSignedField(value, "created_in_block", output.createdInBlock, result)) return false;
if (!readRequiredUnsignedField(value, "value", output.value, result)) return false;
if (!readOptionalBoolField(value, "spent", output.spent, result)) return false;
if (!readOptionalBoolField(value, "unconfirmed_spent", output.unconfirmedSpent, result)) return false;
output.spendable = !output.pending && !output.spent && !output.unconfirmedSpent;
return true;
}
template <typename Result>
bool parseSpendableOutputArray(const json& object,
const std::string& key,
LiteSpendableOutputKind kind,
std::vector<LiteSpendableOutput>& output,
Result& result)
{
if (!object.contains(key) || object.at(key).is_null()) {
return fail(result, LiteResultParserError::MissingField, fieldPath(key), "missing required spendable output array");
}
const auto& values = object.at(key);
if (!requireArray(values, fieldPath(key), result)) return false;
output.clear();
for (std::size_t index = 0; index < values.size(); ++index) {
LiteSpendableOutput parsed;
if (!parseSpendableOutput(values[index], kind, indexPath(fieldPath(key), index), parsed, result)) return false;
output.push_back(std::move(parsed));
}
return true;
}
template <typename Result>
bool parseTransactionOutput(const json& value,
const std::string& path,
LiteTransactionOutput& output,
Result& result)
{
if (!requireObject(value, path, result)) return false;
if (!readRequiredStringField(value, "address", output.address, result)) return false;
if (!readRequiredSignedField(value, "value", output.value, result)) return false;
if (!readOptionalStringField(value, "memo", output.memo, result)) return false;
return true;
}
template <typename Result>
bool parseTransactionRecord(const json& value,
const std::string& path,
LiteTransactionRecord& output,
Result& result)
{
if (!requireObject(value, path, result)) return false;
if (!readRequiredStringField(value, "txid", output.txid, result)) return false;
if (!readRequiredSignedField(value, "datetime", output.datetime, result)) return false;
if (!readOptionalSignedField(value, "block_height", output.blockHeight, result)) return false;
if (!readOptionalBoolField(value, "unconfirmed", output.unconfirmed, result)) return false;
if (value.contains("outgoing_metadata") && !value.at("outgoing_metadata").is_null()) {
const auto& metadata = value.at("outgoing_metadata");
if (!requireArray(metadata, path + ".outgoing_metadata", result)) return false;
output.direction = LiteTransactionDirection::Send;
for (std::size_t index = 0; index < metadata.size(); ++index) {
LiteTransactionOutput parsedOutput;
if (!parseTransactionOutput(metadata[index], path + ".outgoing_metadata[" + std::to_string(index) + "]", parsedOutput, result)) return false;
output.amount += parsedOutput.value;
output.outgoingMetadata.push_back(std::move(parsedOutput));
}
return true;
}
output.direction = LiteTransactionDirection::Receive;
if (!readRequiredStringField(value, "address", output.address, result)) return false;
if (!readRequiredSignedField(value, "amount", output.amount, result)) return false;
if (!readOptionalStringField(value, "memo", output.memo, result)) return false;
if (!readOptionalSignedField(value, "position", output.position, result)) return false;
return true;
}
} // namespace
const char* liteResultCommandName(LiteResultCommand command)
{
switch (command) {
case LiteResultCommand::Info: return "info";
case LiteResultCommand::Height: return "height";
case LiteResultCommand::Balance: return "balance";
case LiteResultCommand::Addresses: return "addresses";
case LiteResultCommand::List: return "list";
case LiteResultCommand::Notes: return "notes";
case LiteResultCommand::SyncStatus: return "syncstatus";
}
return "unknown";
}
const char* liteResultParserErrorName(LiteResultParserError error)
{
switch (error) {
case LiteResultParserError::None: return "none";
case LiteResultParserError::InvalidJson: return "invalid_json";
case LiteResultParserError::ExpectedObject: return "expected_object";
case LiteResultParserError::ExpectedArray: return "expected_array";
case LiteResultParserError::MissingField: return "missing_field";
case LiteResultParserError::InvalidFieldType: return "invalid_field_type";
case LiteResultParserError::InvalidFieldValue: return "invalid_field_value";
}
return "unknown";
}
const char* liteSpendableOutputKindName(LiteSpendableOutputKind kind)
{
switch (kind) {
case LiteSpendableOutputKind::UnspentNote: return "unspent_note";
case LiteSpendableOutputKind::Utxo: return "utxo";
case LiteSpendableOutputKind::PendingNote: return "pending_note";
case LiteSpendableOutputKind::PendingUtxo: return "pending_utxo";
}
return "unknown";
}
const char* liteTransactionDirectionName(LiteTransactionDirection direction)
{
switch (direction) {
case LiteTransactionDirection::Unknown: return "unknown";
case LiteTransactionDirection::Send: return "send";
case LiteTransactionDirection::Receive: return "receive";
}
return "unknown";
}
LiteInfoParseResult parseLiteInfoResponse(const std::string& jsonText)
{
auto result = makeResult<LiteInfoParseResult>(LiteResultCommand::Info);
json parsed;
if (!parseJsonText(jsonText, result, parsed)) return result;
return parseLiteInfoResponse(parsed);
}
LiteInfoParseResult parseLiteInfoResponse(const json& value)
{
auto result = makeResult<LiteInfoParseResult>(LiteResultCommand::Info);
if (!requireObject(value, "$", result)) return result;
if (!readOptionalStringField(value, "chain_name", result.info.chainName, result)) return result;
if (!readOptionalStringField(value, "version", result.info.version, result)) return result;
if (!readOptionalStringField(value, "vendor", result.info.vendor, result)) return result;
if (!readOptionalSignedField(value, "latest_block_height", result.info.latestBlockHeight, result)) return result;
if (!readOptionalSignedField(value, "difficulty", result.info.difficulty, result)) return result;
if (!readOptionalSignedField(value, "longestchain", result.info.longestChain, result)) return result;
if (!readOptionalSignedField(value, "notarized", result.info.notarized, result)) return result;
if (!result.info.latestBlockHeight.has_value()) {
fail(result, LiteResultParserError::MissingField, fieldPath("latest_block_height"), "info response is missing latest_block_height");
return result;
}
succeed(result);
return result;
}
LiteHeightParseResult parseLiteHeightResponse(const std::string& jsonText)
{
auto result = makeResult<LiteHeightParseResult>(LiteResultCommand::Height);
json parsed;
if (!parseJsonText(jsonText, result, parsed)) return result;
return parseLiteHeightResponse(parsed);
}
LiteHeightParseResult parseLiteHeightResponse(const json& value)
{
auto result = makeResult<LiteHeightParseResult>(LiteResultCommand::Height);
std::int64_t parsedHeight = 0;
if (jsonToSigned(value, parsedHeight)) {
result.height.height = parsedHeight;
succeed(result);
return result;
}
if (!requireObject(value, "$", result)) return result;
for (const std::string key : {"height", "latest_block_height", "block_height"}) {
if (!value.contains(key) || value.at(key).is_null()) continue;
if (!jsonToSigned(value.at(key), parsedHeight)) {
fail(result, LiteResultParserError::InvalidFieldValue, fieldPath(key), "height field is not an integer-compatible value");
return result;
}
result.height.height = parsedHeight;
succeed(result);
return result;
}
fail(result, LiteResultParserError::MissingField, "$", "height response has no recognized height field");
return result;
}
LiteBalanceParseResult parseLiteBalanceResponse(const std::string& jsonText)
{
auto result = makeResult<LiteBalanceParseResult>(LiteResultCommand::Balance);
json parsed;
if (!parseJsonText(jsonText, result, parsed)) return result;
return parseLiteBalanceResponse(parsed);
}
LiteBalanceParseResult parseLiteBalanceResponse(const json& value)
{
auto result = makeResult<LiteBalanceParseResult>(LiteResultCommand::Balance);
if (!requireObject(value, "$", result)) return result;
if (!readRequiredUnsignedField(value, "tbalance", result.balance.transparentBalance, result)) return result;
if (!readRequiredUnsignedField(value, "zbalance", result.balance.shieldedBalance, result)) return result;
if (!readRequiredUnsignedField(value, "unconfirmed", result.balance.unconfirmedBalance, result)) return result;
if (!readRequiredUnsignedField(value, "verified_zbalance", result.balance.verifiedShieldedBalance, result)) return result;
if (!readRequiredUnsignedField(value, "spendable_zbalance", result.balance.spendableShieldedBalance, result)) return result;
succeed(result);
return result;
}
LiteAddressesParseResult parseLiteAddressesResponse(const std::string& jsonText)
{
auto result = makeResult<LiteAddressesParseResult>(LiteResultCommand::Addresses);
json parsed;
if (!parseJsonText(jsonText, result, parsed)) return result;
return parseLiteAddressesResponse(parsed);
}
LiteAddressesParseResult parseLiteAddressesResponse(const json& value)
{
auto result = makeResult<LiteAddressesParseResult>(LiteResultCommand::Addresses);
if (!requireObject(value, "$", result)) return result;
if (!readStringArrayField(value, "z_addresses", result.addresses.zAddresses, result)) return result;
if (!readStringArrayField(value, "t_addresses", result.addresses.tAddresses, result)) return result;
succeed(result);
return result;
}
LiteNotesParseResult parseLiteNotesResponse(const std::string& jsonText)
{
auto result = makeResult<LiteNotesParseResult>(LiteResultCommand::Notes);
json parsed;
if (!parseJsonText(jsonText, result, parsed)) return result;
return parseLiteNotesResponse(parsed);
}
LiteNotesParseResult parseLiteNotesResponse(const json& value)
{
auto result = makeResult<LiteNotesParseResult>(LiteResultCommand::Notes);
if (!requireObject(value, "$", result)) return result;
if (!parseSpendableOutputArray(value, "unspent_notes", LiteSpendableOutputKind::UnspentNote, result.notes.unspentNotes, result)) return result;
if (!parseSpendableOutputArray(value, "utxos", LiteSpendableOutputKind::Utxo, result.notes.utxos, result)) return result;
if (!parseSpendableOutputArray(value, "pending_notes", LiteSpendableOutputKind::PendingNote, result.notes.pendingNotes, result)) return result;
if (!parseSpendableOutputArray(value, "pending_utxos", LiteSpendableOutputKind::PendingUtxo, result.notes.pendingUtxos, result)) return result;
succeed(result);
return result;
}
LiteTransactionsParseResult parseLiteTransactionsResponse(const std::string& jsonText)
{
auto result = makeResult<LiteTransactionsParseResult>(LiteResultCommand::List);
json parsed;
if (!parseJsonText(jsonText, result, parsed)) return result;
return parseLiteTransactionsResponse(parsed);
}
LiteTransactionsParseResult parseLiteTransactionsResponse(const json& value)
{
auto result = makeResult<LiteTransactionsParseResult>(LiteResultCommand::List);
if (!requireArray(value, "$", result)) return result;
result.transactions.transactions.clear();
for (std::size_t index = 0; index < value.size(); ++index) {
LiteTransactionRecord record;
if (!parseTransactionRecord(value[index], "$[" + std::to_string(index) + "]", record, result)) return result;
result.transactions.transactions.push_back(std::move(record));
}
succeed(result);
return result;
}
LiteSyncStatusParseResult parseLiteSyncStatusResponse(const std::string& jsonText)
{
auto result = makeResult<LiteSyncStatusParseResult>(LiteResultCommand::SyncStatus);
json parsed;
if (!parseJsonText(jsonText, result, parsed)) return result;
return parseLiteSyncStatusResponse(parsed);
}
LiteSyncStatusParseResult parseLiteSyncStatusResponse(const json& value)
{
auto result = makeResult<LiteSyncStatusParseResult>(LiteResultCommand::SyncStatus);
if (!requireObject(value, "$", result)) return result;
if (!readRequiredUnsignedField(value, "synced_blocks", result.syncStatus.syncedBlocks, result)) return result;
if (!readRequiredUnsignedField(value, "total_blocks", result.syncStatus.totalBlocks, result)) return result;
if (result.syncStatus.totalBlocks > 0) {
result.syncStatus.progress = std::min(1.0, static_cast<double>(result.syncStatus.syncedBlocks) / static_cast<double>(result.syncStatus.totalBlocks));
result.syncStatus.complete = result.syncStatus.syncedBlocks >= result.syncStatus.totalBlocks;
}
succeed(result);
return result;
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,169 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
namespace dragonx::wallet {
enum class LiteResultCommand {
Info,
Height,
Balance,
Addresses,
List,
Notes,
SyncStatus,
};
enum class LiteResultParserError {
None,
InvalidJson,
ExpectedObject,
ExpectedArray,
MissingField,
InvalidFieldType,
InvalidFieldValue,
};
enum class LiteSpendableOutputKind {
UnspentNote,
Utxo,
PendingNote,
PendingUtxo,
};
enum class LiteTransactionDirection {
Unknown,
Send,
Receive,
};
struct LiteResultParserIssue {
std::string path;
std::string message;
};
struct LiteParseResultBase {
bool ok = false;
LiteResultCommand command = LiteResultCommand::Info;
LiteResultParserError error = LiteResultParserError::None;
std::string errorPath;
std::string errorMessage;
std::vector<LiteResultParserIssue> issues;
};
struct LiteInfoResponse {
std::optional<std::string> chainName;
std::optional<std::string> version;
std::optional<std::string> vendor;
std::optional<std::int64_t> latestBlockHeight;
std::optional<std::int64_t> difficulty;
std::optional<std::int64_t> longestChain;
std::optional<std::int64_t> notarized;
};
struct LiteHeightResponse {
std::int64_t height = 0;
};
struct LiteBalanceResponse {
std::uint64_t transparentBalance = 0;
std::uint64_t shieldedBalance = 0;
std::uint64_t unconfirmedBalance = 0;
std::uint64_t verifiedShieldedBalance = 0;
std::uint64_t spendableShieldedBalance = 0;
};
struct LiteAddressesResponse {
std::vector<std::string> zAddresses;
std::vector<std::string> tAddresses;
};
struct LiteSpendableOutput {
LiteSpendableOutputKind kind = LiteSpendableOutputKind::UnspentNote;
std::string address;
std::string createdInTxid;
std::optional<std::int64_t> createdInBlock;
std::uint64_t value = 0;
bool spent = false;
bool unconfirmedSpent = false;
bool pending = false;
bool spendable = false;
};
struct LiteNotesResponse {
std::vector<LiteSpendableOutput> unspentNotes;
std::vector<LiteSpendableOutput> utxos;
std::vector<LiteSpendableOutput> pendingNotes;
std::vector<LiteSpendableOutput> pendingUtxos;
};
struct LiteTransactionOutput {
std::string address;
std::int64_t value = 0;
std::string memo;
};
struct LiteTransactionRecord {
std::string txid;
std::int64_t datetime = 0;
std::optional<std::int64_t> blockHeight;
bool unconfirmed = false;
LiteTransactionDirection direction = LiteTransactionDirection::Unknown;
std::string address;
std::int64_t amount = 0;
std::string memo;
std::optional<std::int64_t> position;
std::vector<LiteTransactionOutput> outgoingMetadata;
};
struct LiteTransactionsResponse {
std::vector<LiteTransactionRecord> transactions;
};
struct LiteSyncStatusResponse {
std::uint64_t syncedBlocks = 0;
std::uint64_t totalBlocks = 0;
double progress = 0.0;
bool complete = false;
};
struct LiteInfoParseResult : LiteParseResultBase { LiteInfoResponse info; };
struct LiteHeightParseResult : LiteParseResultBase { LiteHeightResponse height; };
struct LiteBalanceParseResult : LiteParseResultBase { LiteBalanceResponse balance; };
struct LiteAddressesParseResult : LiteParseResultBase { LiteAddressesResponse addresses; };
struct LiteNotesParseResult : LiteParseResultBase { LiteNotesResponse notes; };
struct LiteTransactionsParseResult : LiteParseResultBase { LiteTransactionsResponse transactions; };
struct LiteSyncStatusParseResult : LiteParseResultBase { LiteSyncStatusResponse syncStatus; };
const char* liteResultCommandName(LiteResultCommand command);
const char* liteResultParserErrorName(LiteResultParserError error);
const char* liteSpendableOutputKindName(LiteSpendableOutputKind kind);
const char* liteTransactionDirectionName(LiteTransactionDirection direction);
LiteInfoParseResult parseLiteInfoResponse(const std::string& jsonText);
LiteInfoParseResult parseLiteInfoResponse(const nlohmann::json& value);
LiteHeightParseResult parseLiteHeightResponse(const std::string& jsonText);
LiteHeightParseResult parseLiteHeightResponse(const nlohmann::json& value);
LiteBalanceParseResult parseLiteBalanceResponse(const std::string& jsonText);
LiteBalanceParseResult parseLiteBalanceResponse(const nlohmann::json& value);
LiteAddressesParseResult parseLiteAddressesResponse(const std::string& jsonText);
LiteAddressesParseResult parseLiteAddressesResponse(const nlohmann::json& value);
LiteNotesParseResult parseLiteNotesResponse(const std::string& jsonText);
LiteNotesParseResult parseLiteNotesResponse(const nlohmann::json& value);
LiteTransactionsParseResult parseLiteTransactionsResponse(const std::string& jsonText);
LiteTransactionsParseResult parseLiteTransactionsResponse(const nlohmann::json& value);
LiteSyncStatusParseResult parseLiteSyncStatusResponse(const std::string& jsonText);
LiteSyncStatusParseResult parseLiteSyncStatusResponse(const nlohmann::json& value);
} // namespace dragonx::wallet

View File

@@ -0,0 +1,371 @@
#include "wallet/lite_sync_service.h"
#include <algorithm>
#include <cctype>
#include <limits>
#include <utility>
namespace dragonx::wallet {
namespace {
std::string trimSyncCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
std::uint64_t effectiveStagnantThreshold(std::uint64_t threshold)
{
return threshold == 0 ? 1 : threshold;
}
WalletBackendStatus makeSyncScaffoldStatus(WalletBackendState state,
std::string message,
const LiteSyncStatusResponse* syncStatus = nullptr)
{
WalletBackendStatus status;
status.state = state;
status.message = std::move(message);
if (syncStatus) {
status.walletHeight = static_cast<int>(std::min<std::uint64_t>(syncStatus->syncedBlocks, static_cast<std::uint64_t>(std::numeric_limits<int>::max())));
status.chainHeight = static_cast<int>(std::min<std::uint64_t>(syncStatus->totalBlocks, static_cast<std::uint64_t>(std::numeric_limits<int>::max())));
status.syncProgress = syncStatus->progress;
}
return status;
}
} // namespace
const char* liteSyncOperationName(LiteSyncOperation operation)
{
switch (operation) {
case LiteSyncOperation::StartSync: return "StartSync";
case LiteSyncOperation::PollSyncStatus: return "PollSyncStatus";
}
return "Unknown";
}
const char* liteSyncStartModeName(LiteSyncStartMode mode)
{
switch (mode) {
case LiteSyncStartMode::Startup: return "Startup";
case LiteSyncStartMode::Restore: return "Restore";
case LiteSyncStartMode::Rescan: return "Rescan";
case LiteSyncStartMode::Recovery: return "Recovery";
}
return "Unknown";
}
const char* liteSyncAvailabilityName(LiteSyncAvailability availability)
{
switch (availability) {
case LiteSyncAvailability::Ready: return "Ready";
case LiteSyncAvailability::UnsupportedBuild: return "UnsupportedBuild";
case LiteSyncAvailability::BackendUnavailable: return "BackendUnavailable";
case LiteSyncAvailability::BridgeUnavailable: return "BridgeUnavailable";
case LiteSyncAvailability::BridgeCallsDisabled: return "BridgeCallsDisabled";
case LiteSyncAvailability::NoUsableServer: return "NoUsableServer";
}
return "Unknown";
}
const char* liteSyncRecoveryDecisionKindName(LiteSyncRecoveryDecisionKind kind)
{
switch (kind) {
case LiteSyncRecoveryDecisionKind::KeepPolling: return "KeepPolling";
case LiteSyncRecoveryDecisionKind::SyncComplete: return "SyncComplete";
case LiteSyncRecoveryDecisionKind::Stuck: return "Stuck";
case LiteSyncRecoveryDecisionKind::ReorgDetected: return "ReorgDetected";
case LiteSyncRecoveryDecisionKind::InvalidStatus: return "InvalidStatus";
}
return "Unknown";
}
WalletBackendStatus walletStatusFromLiteSyncStatus(const LiteSyncStatusResponse& syncStatus)
{
if (syncStatus.complete) {
return makeSyncScaffoldStatus(
WalletBackendState::Ready,
"lite syncstatus reports sync complete",
&syncStatus);
}
return makeSyncScaffoldStatus(
WalletBackendState::Syncing,
"lite syncstatus reports sync in progress",
&syncStatus);
}
LiteSyncRecoveryDecision evaluateLiteSyncRecovery(const LiteSyncRecoveryInput& input)
{
if (input.totalBlocks > 0 && input.syncedBlocks > input.totalBlocks) {
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::InvalidStatus,
false,
false,
false,
false,
true,
"synced block count is greater than total block count"
};
}
if (input.havePreviousSyncedBlocks && input.syncedBlocks < input.previousSyncedBlocks) {
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::ReorgDetected,
false,
true,
true,
true,
false,
"synced block count moved backwards; model clear/rescan recovery"
};
}
if (input.totalBlocks > 0 && input.syncedBlocks >= input.totalBlocks) {
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::SyncComplete,
false,
false,
false,
false,
false,
"syncstatus reports all blocks synced"
};
}
const auto threshold = effectiveStagnantThreshold(input.stagnantPollThreshold);
if (input.havePreviousSyncedBlocks &&
input.syncedBlocks == input.previousSyncedBlocks &&
input.stagnantPollCount >= threshold) {
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::Stuck,
false,
false,
false,
true,
false,
"synced block count has not advanced past the configured threshold"
};
}
return LiteSyncRecoveryDecision{
LiteSyncRecoveryDecisionKind::KeepPolling,
true,
false,
false,
false,
false,
"syncstatus is usable; keep polling"
};
}
LiteSyncService::LiteSyncService(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteSyncServiceOptions options)
: capabilities_(capabilities),
connectionSettings_(std::move(connectionSettings)),
bridge_(std::move(bridge)),
options_(options)
{
}
LiteSyncAvailability LiteSyncService::availability() const
{
if (!isLiteBuild(capabilities_)) return LiteSyncAvailability::UnsupportedBuild;
if (!supportsLiteBackend(capabilities_)) return LiteSyncAvailability::BackendUnavailable;
if (!bridge_.available()) return LiteSyncAvailability::BridgeUnavailable;
if (!selectLiteServer(connectionSettings_).ok) return LiteSyncAvailability::NoUsableServer;
if (!options_.allowSyncStatusBridgeCalls) return LiteSyncAvailability::BridgeCallsDisabled;
return LiteSyncAvailability::Ready;
}
WalletBackendStatus LiteSyncService::status() const
{
const auto currentAvailability = availability();
if (currentAvailability == LiteSyncAvailability::NoUsableServer) {
return statusFor(currentAvailability, selectLiteServer(connectionSettings_).error);
}
return statusFor(currentAvailability);
}
LiteSyncPlan LiteSyncService::planStartSync(const LiteSyncStartRequest& request) const
{
return makePlan(
LiteSyncOperation::StartSync,
request.serverUrl,
false,
request.mode,
request.forceRescan,
request.afterRestore);
}
LiteSyncPlan LiteSyncService::planSyncStatus(const LiteSyncStatusRequest& request) const
{
return makePlan(
LiteSyncOperation::PollSyncStatus,
request.serverUrl,
options_.allowSyncStatusBridgeCalls,
LiteSyncStartMode::Startup,
false,
false);
}
LiteSyncStartResult LiteSyncService::startSync(const LiteSyncStartRequest& request)
{
const auto plan = planStartSync(request);
if (!plan.ok) return blockedStartResult(plan, makeSyncScaffoldStatus(WalletBackendState::Error, plan.error));
const auto currentAvailability = availability();
if (currentAvailability == LiteSyncAvailability::UnsupportedBuild ||
currentAvailability == LiteSyncAvailability::BackendUnavailable ||
currentAvailability == LiteSyncAvailability::BridgeUnavailable ||
currentAvailability == LiteSyncAvailability::NoUsableServer) {
return blockedStartResult(plan, status());
}
return blockedStartResult(
plan,
makeSyncScaffoldStatus(WalletBackendState::Unavailable, "lite sync start execution is not implemented"));
}
LiteSyncStatusResult LiteSyncService::pollSyncStatus(const LiteSyncStatusRequest& request)
{
const auto plan = planSyncStatus(request);
if (!plan.ok) return blockedStatusResult(plan, makeSyncScaffoldStatus(WalletBackendState::Error, plan.error));
if (availability() != LiteSyncAvailability::Ready) return blockedStatusResult(plan, status());
LiteSyncStatusResult result;
result.plan = plan;
result.attempted = true;
const auto bridgeCall = bridge_.execute(plan.command, plan.args);
result.bridgeResponseRedacted = bridgeCall.ok || !bridgeCall.error.empty() ? "<redacted>" : "<empty>";
if (!bridgeCall.ok) {
result.status = makeSyncScaffoldStatus(WalletBackendState::Error, "lite syncstatus bridge call failed");
result.error = result.status.message;
return result;
}
result.bridgeAccepted = true;
const auto parsed = parseLiteSyncStatusResponse(bridgeCall.value);
if (!parsed.ok) {
result.parserError = parsed.error;
result.status = makeSyncScaffoldStatus(WalletBackendState::Error, "lite syncstatus response could not be parsed");
result.error = result.status.message;
return result;
}
result.ok = true;
result.parserError = LiteResultParserError::None;
result.syncStatus = parsed.syncStatus;
result.status = walletStatusFromLiteSyncStatus(result.syncStatus);
return result;
}
LiteServerSelectionResult LiteSyncService::selectServerForRequest(const std::string& serverUrl) const
{
const auto overrideUrl = trimSyncCopy(serverUrl);
if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite sync server URL is not usable"};
}
return LiteServerSelectionResult{
true,
LiteServerEndpoint{overrideUrl, "Request", true},
0,
true,
{}
};
}
return selectLiteServer(connectionSettings_);
}
LiteSyncPlan LiteSyncService::makePlan(LiteSyncOperation operation,
const std::string& serverUrl,
bool bridgeExecutionAllowed,
LiteSyncStartMode startMode,
bool forceRescan,
bool afterRestore) const
{
LiteSyncPlan plan;
plan.operation = operation;
plan.startMode = startMode;
plan.bridgeExecutionAllowed = bridgeExecutionAllowed;
plan.forceRescan = forceRescan;
plan.afterRestore = afterRestore;
plan.command = operation == LiteSyncOperation::StartSync ? "sync" : "syncstatus";
auto selection = selectServerForRequest(serverUrl);
if (!selection.ok) {
plan.error = selection.error;
return plan;
}
plan.ok = true;
plan.server = selection.server;
plan.serverIndex = selection.serverIndex;
plan.customServer = selection.customServer;
return plan;
}
WalletBackendStatus LiteSyncService::statusFor(LiteSyncAvailability availability,
const std::string& detail) const
{
switch (availability) {
case LiteSyncAvailability::Ready:
return makeSyncScaffoldStatus(
WalletBackendState::Disconnected,
detail.empty() ? "lite sync scaffold ready; sync is not started" : detail);
case LiteSyncAvailability::UnsupportedBuild:
return makeSyncScaffoldStatus(
WalletBackendState::Unavailable,
"lite sync service is unsupported in full-node builds");
case LiteSyncAvailability::BackendUnavailable:
return makeSyncScaffoldStatus(
WalletBackendState::Unavailable,
"lite backend is not linked");
case LiteSyncAvailability::BridgeUnavailable:
return makeSyncScaffoldStatus(
WalletBackendState::Unavailable,
detail.empty() ? bridge_.unavailableReason() : detail);
case LiteSyncAvailability::BridgeCallsDisabled:
return makeSyncScaffoldStatus(
WalletBackendState::Unavailable,
"lite syncstatus bridge calls are disabled");
case LiteSyncAvailability::NoUsableServer:
return makeSyncScaffoldStatus(
WalletBackendState::Error,
detail.empty() ? "no usable lite servers are configured" : detail);
}
return makeSyncScaffoldStatus(WalletBackendState::Unavailable, "unknown lite sync state");
}
LiteSyncStartResult LiteSyncService::blockedStartResult(const LiteSyncPlan& plan,
const WalletBackendStatus& blockedStatus) const
{
LiteSyncStartResult result;
result.plan = plan;
result.status = blockedStatus;
result.error = blockedStatus.message;
return result;
}
LiteSyncStatusResult LiteSyncService::blockedStatusResult(const LiteSyncPlan& plan,
const WalletBackendStatus& blockedStatus) const
{
LiteSyncStatusResult result;
result.plan = plan;
result.status = blockedStatus;
result.error = blockedStatus.message;
return result;
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,162 @@
#pragma once
#include "lite_client_bridge.h"
#include "lite_connection_service.h"
#include "lite_result_parsers.h"
#include "wallet_backend.h"
#include "wallet_capabilities.h"
#include <cstddef>
#include <cstdint>
#include <string>
namespace dragonx::wallet {
enum class LiteSyncOperation {
StartSync,
PollSyncStatus,
};
enum class LiteSyncStartMode {
Startup,
Restore,
Rescan,
Recovery,
};
enum class LiteSyncAvailability {
Ready,
UnsupportedBuild,
BackendUnavailable,
BridgeUnavailable,
BridgeCallsDisabled,
NoUsableServer,
};
enum class LiteSyncRecoveryDecisionKind {
KeepPolling,
SyncComplete,
Stuck,
ReorgDetected,
InvalidStatus,
};
struct LiteSyncStartRequest {
LiteSyncStartMode mode = LiteSyncStartMode::Startup;
std::string serverUrl;
bool forceRescan = false;
bool afterRestore = false;
};
struct LiteSyncStatusRequest {
std::string serverUrl;
};
struct LiteSyncPlan {
bool ok = false;
LiteSyncOperation operation = LiteSyncOperation::StartSync;
LiteSyncStartMode startMode = LiteSyncStartMode::Startup;
LiteServerEndpoint server;
std::size_t serverIndex = 0;
bool customServer = false;
bool bridgeExecutionAllowed = false;
std::string command;
std::string args;
bool forceRescan = false;
bool afterRestore = false;
std::string error;
};
struct LiteSyncStartResult {
bool ok = false;
bool attempted = false;
bool syncStarted = false;
LiteSyncPlan plan;
WalletBackendStatus status;
std::string error;
};
struct LiteSyncStatusResult {
bool ok = false;
bool attempted = false;
bool bridgeAccepted = false;
LiteSyncPlan plan;
LiteSyncStatusResponse syncStatus;
LiteResultParserError parserError = LiteResultParserError::None;
WalletBackendStatus status;
std::string bridgeResponseRedacted = "<empty>";
std::string error;
};
struct LiteSyncRecoveryInput {
bool havePreviousSyncedBlocks = false;
std::uint64_t previousSyncedBlocks = 0;
std::uint64_t syncedBlocks = 0;
std::uint64_t totalBlocks = 0;
std::uint64_t stagnantPollCount = 0;
std::uint64_t stagnantPollThreshold = 10;
};
struct LiteSyncRecoveryDecision {
LiteSyncRecoveryDecisionKind kind = LiteSyncRecoveryDecisionKind::KeepPolling;
bool shouldPollAgain = true;
bool shouldClear = false;
bool shouldRescan = false;
bool shouldRestartSync = false;
bool requiresUserAttention = false;
std::string reason;
};
struct LiteSyncServiceOptions {
bool allowSyncStatusBridgeCalls = false;
};
const char* liteSyncOperationName(LiteSyncOperation operation);
const char* liteSyncStartModeName(LiteSyncStartMode mode);
const char* liteSyncAvailabilityName(LiteSyncAvailability availability);
const char* liteSyncRecoveryDecisionKindName(LiteSyncRecoveryDecisionKind kind);
WalletBackendStatus walletStatusFromLiteSyncStatus(const LiteSyncStatusResponse& syncStatus);
LiteSyncRecoveryDecision evaluateLiteSyncRecovery(const LiteSyncRecoveryInput& input);
class LiteSyncService {
public:
LiteSyncService(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteSyncServiceOptions options = {});
const WalletCapabilities& capabilities() const { return capabilities_; }
const LiteConnectionSettings& connectionSettings() const { return connectionSettings_; }
const LiteSyncServiceOptions& options() const { return options_; }
LiteSyncAvailability availability() const;
WalletBackendStatus status() const;
LiteSyncPlan planStartSync(const LiteSyncStartRequest& request) const;
LiteSyncPlan planSyncStatus(const LiteSyncStatusRequest& request) const;
LiteSyncStartResult startSync(const LiteSyncStartRequest& request);
LiteSyncStatusResult pollSyncStatus(const LiteSyncStatusRequest& request);
private:
LiteServerSelectionResult selectServerForRequest(const std::string& serverUrl) const;
LiteSyncPlan makePlan(LiteSyncOperation operation,
const std::string& serverUrl,
bool bridgeExecutionAllowed,
LiteSyncStartMode startMode,
bool forceRescan,
bool afterRestore) const;
WalletBackendStatus statusFor(LiteSyncAvailability availability,
const std::string& detail = {}) const;
LiteSyncStartResult blockedStartResult(const LiteSyncPlan& plan,
const WalletBackendStatus& blockedStatus) const;
LiteSyncStatusResult blockedStatusResult(const LiteSyncPlan& plan,
const WalletBackendStatus& blockedStatus) const;
WalletCapabilities capabilities_;
LiteConnectionSettings connectionSettings_;
LiteClientBridge bridge_;
LiteSyncServiceOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,172 @@
#include "wallet/lite_wallet_app_refresh_coordinator.h"
#include <utility>
namespace dragonx::wallet {
namespace {
void addIssue(LiteWalletAppRefreshCoordinationResult& result,
LiteWalletAppRefreshCoordinationIssue issue,
std::string message)
{
result.issues.push_back(LiteWalletAppRefreshCoordinationIssueInfo{issue, std::move(message)});
}
std::string refreshFailureMessage(const LiteWalletRefreshServiceResult& refreshResult)
{
if (!refreshResult.error.empty()) return refreshResult.error;
if (!refreshResult.status.message.empty()) return refreshResult.status.message;
return "lite wallet refresh service result is not successful";
}
void copyExecutionSummary(LiteWalletAppRefreshCoordinationResult& result)
{
result.stateMutationRequested = result.executionResult.stateMutationRequested;
result.stateMutationAllowed = result.applyPlan.stateMutationAllowed ||
result.executionResult.stateMutationAllowed;
result.stateMutated = result.executionResult.stateMutated;
result.walletStateWritten = result.stateMutated;
result.dryRunOnly = result.applyPlan.dryRunOnly && result.executionResult.dryRunOnly;
result.noNetwork = result.executionResult.noNetwork;
result.fieldPlanCount = result.executionResult.fieldPlanCount;
result.collectionPlanCount = result.executionResult.collectionPlanCount;
result.plannedChangeCount = result.executionResult.plannedChangeCount;
result.planIssueCount = result.executionResult.planIssueCount;
result.executionIssueCount = result.executionResult.issues.size();
}
} // namespace
const char* liteWalletAppRefreshCoordinationStatusName(
LiteWalletAppRefreshCoordinationStatus status)
{
switch (status) {
case LiteWalletAppRefreshCoordinationStatus::DryRunReported: return "DryRunReported";
case LiteWalletAppRefreshCoordinationStatus::Rejected: return "Rejected";
case LiteWalletAppRefreshCoordinationStatus::ApplyUnavailable: return "ApplyUnavailable";
}
return "Unknown";
}
const char* liteWalletAppRefreshCoordinationIssueName(
LiteWalletAppRefreshCoordinationIssue issue)
{
switch (issue) {
case LiteWalletAppRefreshCoordinationIssue::RefreshResultFailed: return "RefreshResultFailed";
case LiteWalletAppRefreshCoordinationIssue::FullNodeRefreshDelegated: return "FullNodeRefreshDelegated";
case LiteWalletAppRefreshCoordinationIssue::MappingFailed: return "MappingFailed";
case LiteWalletAppRefreshCoordinationIssue::ApplyPlanFailed: return "ApplyPlanFailed";
case LiteWalletAppRefreshCoordinationIssue::ApplyExecutionRejected: return "ApplyExecutionRejected";
case LiteWalletAppRefreshCoordinationIssue::StateMutationDisabled: return "StateMutationDisabled";
case LiteWalletAppRefreshCoordinationIssue::StateMutationImplementationMissing: return "StateMutationImplementationMissing";
}
return "Unknown";
}
LiteWalletAppRefreshCoordinationResult coordinateLiteWalletAppRefresh(
const LiteWalletRefreshServiceResult& refreshResult,
const dragonx::WalletState& walletState,
LiteWalletAppRefreshCoordinatorOptions options)
{
LiteWalletAppRefreshCoordinationResult result;
result.route = refreshResult.plan.route;
result.refreshStatus = refreshResult.status;
result.refreshAttempted = refreshResult.attempted;
result.fullNodeRefreshDelegated = refreshResult.fullNodeRefreshDelegated;
result.liteGatewayCalled = refreshResult.liteGatewayCalled;
result.stateMutationRequested = options.executorOptions.requestStateMutation;
if (refreshResult.fullNodeRefreshDelegated) {
addIssue(result,
LiteWalletAppRefreshCoordinationIssue::FullNodeRefreshDelegated,
"full-node refresh remains delegated; lite app refresh coordinator did not run");
result.error = result.issues.back().message;
return result;
}
if (!refreshResult.ok) {
addIssue(result,
LiteWalletAppRefreshCoordinationIssue::RefreshResultFailed,
refreshFailureMessage(refreshResult));
result.error = result.issues.back().message;
return result;
}
result.refreshAccepted = true;
result.mapResult = mapLiteWalletRefreshServiceResult(refreshResult);
result.mapIssueCount = result.mapResult.issues.size();
if (!result.mapResult.ok) {
addIssue(result,
LiteWalletAppRefreshCoordinationIssue::MappingFailed,
result.mapResult.error.empty()
? "lite wallet refresh service result could not be mapped"
: result.mapResult.error);
result.error = result.issues.back().message;
return result;
}
result.mapped = true;
result.successfulCommandCount = result.mapResult.model.successfulCommandCount;
result.applyPlan = planLiteWalletStateApply(result.mapResult.model, walletState);
if (!result.applyPlan.ok) {
result.fieldPlanCount = result.applyPlan.fieldPlans.size();
result.collectionPlanCount = result.applyPlan.collectionPlans.size();
result.planIssueCount = result.applyPlan.issues.size();
addIssue(result,
LiteWalletAppRefreshCoordinationIssue::ApplyPlanFailed,
result.applyPlan.error.empty()
? "lite WalletState apply plan could not be built"
: result.applyPlan.error);
result.error = result.issues.back().message;
return result;
}
result.planned = true;
LiteWalletStateApplyExecutor executor(options.executorOptions);
result.executionResult = executor.execute(result.applyPlan);
result.executionReported = true;
copyExecutionSummary(result);
if (!result.executionResult.ok) {
result.error = result.executionResult.error;
if (result.executionResult.status == LiteWalletStateApplyExecutionStatus::ImplementationMissing) {
result.status = LiteWalletAppRefreshCoordinationStatus::ApplyUnavailable;
addIssue(result,
LiteWalletAppRefreshCoordinationIssue::StateMutationImplementationMissing,
result.executionResult.error.empty()
? "real lite WalletState application requires a future explicit implementation"
: result.executionResult.error);
} else {
result.status = LiteWalletAppRefreshCoordinationStatus::Rejected;
addIssue(result,
LiteWalletAppRefreshCoordinationIssue::ApplyExecutionRejected,
result.executionResult.error.empty()
? "lite WalletState apply execution report was rejected"
: result.executionResult.error);
}
if (result.error.empty()) result.error = result.issues.back().message;
return result;
}
result.ok = true;
result.status = LiteWalletAppRefreshCoordinationStatus::DryRunReported;
addIssue(result,
LiteWalletAppRefreshCoordinationIssue::StateMutationDisabled,
"lite app refresh coordinator produced a dry-run report only; WalletState was not written");
return result;
}
LiteWalletAppRefreshCoordinator::LiteWalletAppRefreshCoordinator(
LiteWalletAppRefreshCoordinatorOptions options)
: options_(options)
{
}
LiteWalletAppRefreshCoordinationResult LiteWalletAppRefreshCoordinator::coordinate(
const LiteWalletRefreshServiceResult& refreshResult,
const dragonx::WalletState& walletState) const
{
return coordinateLiteWalletAppRefresh(refreshResult, walletState, options_);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,96 @@
#pragma once
#include "data/wallet_state.h"
#include "lite_wallet_refresh_service.h"
#include "lite_wallet_state_apply_executor.h"
#include "lite_wallet_state_mapper.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletAppRefreshCoordinationStatus {
DryRunReported,
Rejected,
ApplyUnavailable,
};
enum class LiteWalletAppRefreshCoordinationIssue {
RefreshResultFailed,
FullNodeRefreshDelegated,
MappingFailed,
ApplyPlanFailed,
ApplyExecutionRejected,
StateMutationDisabled,
StateMutationImplementationMissing,
};
struct LiteWalletAppRefreshCoordinatorOptions {
LiteWalletStateApplyExecutorOptions executorOptions;
};
struct LiteWalletAppRefreshCoordinationIssueInfo {
LiteWalletAppRefreshCoordinationIssue issue = LiteWalletAppRefreshCoordinationIssue::StateMutationDisabled;
std::string message;
};
struct LiteWalletAppRefreshCoordinationResult {
bool ok = false;
bool refreshAccepted = false;
bool refreshAttempted = false;
bool fullNodeRefreshDelegated = false;
bool liteGatewayCalled = false;
bool mapped = false;
bool planned = false;
bool executionReported = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool stateMutationRequested = false;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
LiteWalletAppRefreshCoordinationStatus status = LiteWalletAppRefreshCoordinationStatus::Rejected;
LiteWalletRefreshRouteKind route = LiteWalletRefreshRouteKind::Unavailable;
WalletBackendStatus refreshStatus;
std::size_t successfulCommandCount = 0;
std::size_t mapIssueCount = 0;
std::size_t fieldPlanCount = 0;
std::size_t collectionPlanCount = 0;
std::size_t plannedChangeCount = 0;
std::size_t planIssueCount = 0;
std::size_t executionIssueCount = 0;
LiteWalletStateMapResult mapResult;
LiteWalletStateApplyPlan applyPlan;
LiteWalletStateApplyExecutionResult executionResult;
std::vector<LiteWalletAppRefreshCoordinationIssueInfo> issues;
std::string error;
};
const char* liteWalletAppRefreshCoordinationStatusName(
LiteWalletAppRefreshCoordinationStatus status);
const char* liteWalletAppRefreshCoordinationIssueName(
LiteWalletAppRefreshCoordinationIssue issue);
LiteWalletAppRefreshCoordinationResult coordinateLiteWalletAppRefresh(
const LiteWalletRefreshServiceResult& refreshResult,
const dragonx::WalletState& walletState,
LiteWalletAppRefreshCoordinatorOptions options = {});
class LiteWalletAppRefreshCoordinator {
public:
explicit LiteWalletAppRefreshCoordinator(LiteWalletAppRefreshCoordinatorOptions options = {});
LiteWalletAppRefreshCoordinationResult coordinate(
const LiteWalletRefreshServiceResult& refreshResult,
const dragonx::WalletState& walletState) const;
private:
LiteWalletAppRefreshCoordinatorOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,197 @@
#include "wallet/lite_wallet_app_refresh_orchestrator.h"
#include <utility>
namespace dragonx::wallet {
namespace {
void addIssue(LiteWalletAppRefreshOrchestrationResult& result,
LiteWalletAppRefreshOrchestrationIssue issue,
std::string message)
{
result.issues.push_back(LiteWalletAppRefreshOrchestrationIssueInfo{issue, std::move(message)});
}
bool readinessReportIsUnsafe(const LiteWalletRefreshReadinessResult& readiness)
{
return !readiness.dryRunOnly ||
!readiness.noNetwork ||
readiness.stateMutationRequested ||
readiness.stateMutationAllowed ||
readiness.stateMutated ||
readiness.walletStateWritten;
}
std::string readinessErrorOrDefault(const LiteWalletRefreshReadinessResult& readiness)
{
return readiness.error.empty()
? "lite refresh readiness report is not eligible"
: readiness.error;
}
LiteWalletAppRefreshOrchestrationResult blockedResult(
LiteWalletAppRefreshOrchestrationResult result,
LiteWalletAppRefreshOrchestrationIssue issue,
std::string message)
{
result.status = LiteWalletAppRefreshOrchestrationStatus::Blocked;
result.blocked = true;
addIssue(result, issue, std::move(message));
result.error = result.issues.back().message;
return result;
}
LiteWalletAppRefreshOrchestrationResult skippedResult(
LiteWalletAppRefreshOrchestrationResult result,
LiteWalletAppRefreshOrchestrationIssue issue,
std::string message)
{
result.ok = true;
result.status = LiteWalletAppRefreshOrchestrationStatus::Skipped;
result.skipped = true;
addIssue(result, issue, std::move(message));
return result;
}
std::size_t effectiveMaxQueueDepth(const LiteWalletAppRefreshScheduleInput& schedule,
const LiteWalletAppRefreshOrchestratorOptions& options)
{
return schedule.maxQueueDepth > 0 ? schedule.maxQueueDepth : options.maxQueueDepth;
}
} // namespace
const char* liteWalletAppRefreshOrchestrationStatusName(
LiteWalletAppRefreshOrchestrationStatus status)
{
switch (status) {
case LiteWalletAppRefreshOrchestrationStatus::Queued: return "Queued";
case LiteWalletAppRefreshOrchestrationStatus::Skipped: return "Skipped";
case LiteWalletAppRefreshOrchestrationStatus::Blocked: return "Blocked";
}
return "Unknown";
}
const char* liteWalletAppRefreshScheduleTriggerName(
LiteWalletAppRefreshScheduleTrigger trigger)
{
switch (trigger) {
case LiteWalletAppRefreshScheduleTrigger::Periodic: return "Periodic";
case LiteWalletAppRefreshScheduleTrigger::Manual: return "Manual";
case LiteWalletAppRefreshScheduleTrigger::Startup: return "Startup";
case LiteWalletAppRefreshScheduleTrigger::WalletOpened: return "WalletOpened";
}
return "Unknown";
}
const char* liteWalletAppRefreshOrchestrationIssueName(
LiteWalletAppRefreshOrchestrationIssue issue)
{
switch (issue) {
case LiteWalletAppRefreshOrchestrationIssue::ReadinessRejected: return "ReadinessRejected";
case LiteWalletAppRefreshOrchestrationIssue::UnsafeReadinessReport: return "UnsafeReadinessReport";
case LiteWalletAppRefreshOrchestrationIssue::SchedulerDisabled: return "SchedulerDisabled";
case LiteWalletAppRefreshOrchestrationIssue::RefreshNotDue: return "RefreshNotDue";
case LiteWalletAppRefreshOrchestrationIssue::RefreshAlreadyQueued: return "RefreshAlreadyQueued";
case LiteWalletAppRefreshOrchestrationIssue::RefreshInProgress: return "RefreshInProgress";
case LiteWalletAppRefreshOrchestrationIssue::QueuePressure: return "QueuePressure";
}
return "Unknown";
}
LiteWalletAppRefreshOrchestrationResult orchestrateLiteWalletAppRefresh(
const LiteWalletRefreshReadinessResult& readiness,
const LiteWalletAppRefreshScheduleInput& schedule,
LiteWalletAppRefreshOrchestratorOptions options)
{
LiteWalletAppRefreshOrchestrationResult result;
result.readinessResult = readiness;
result.route = readiness.route;
result.dryRunOnly = readiness.dryRunOnly;
result.noNetwork = readiness.noNetwork;
result.stateMutationRequested = readiness.stateMutationRequested;
result.stateMutationAllowed = readiness.stateMutationAllowed;
result.stateMutated = readiness.stateMutated;
result.walletStateWritten = readiness.walletStateWritten;
result.trigger = schedule.trigger;
result.schedulerEnabled = schedule.schedulerEnabled;
result.refreshDue = schedule.refreshDue;
result.forceRefresh = schedule.forceRefresh;
result.refreshAlreadyQueued = schedule.refreshAlreadyQueued;
result.refreshInProgress = schedule.refreshInProgress;
result.queueDepth = schedule.queueDepth;
result.maxQueueDepth = effectiveMaxQueueDepth(schedule, options);
if (options.requireEligibleReadiness && (!readiness.ok || !readiness.eligibleForFutureUiRefresh)) {
return blockedResult(
std::move(result),
LiteWalletAppRefreshOrchestrationIssue::ReadinessRejected,
readinessErrorOrDefault(readiness));
}
if (readinessReportIsUnsafe(readiness)) {
return blockedResult(
std::move(result),
LiteWalletAppRefreshOrchestrationIssue::UnsafeReadinessReport,
"lite refresh readiness report is not a no-network dry-run report");
}
result.readinessAccepted = true;
if (!schedule.schedulerEnabled) {
return blockedResult(
std::move(result),
LiteWalletAppRefreshOrchestrationIssue::SchedulerDisabled,
"lite app refresh scheduler is disabled");
}
result.schedulerAccepted = true;
if (result.maxQueueDepth > 0 && schedule.queueDepth >= result.maxQueueDepth) {
return blockedResult(
std::move(result),
LiteWalletAppRefreshOrchestrationIssue::QueuePressure,
"lite app refresh queue is at capacity");
}
if (schedule.refreshAlreadyQueued) {
return skippedResult(
std::move(result),
LiteWalletAppRefreshOrchestrationIssue::RefreshAlreadyQueued,
"lite app refresh is already queued");
}
if (schedule.refreshInProgress) {
return skippedResult(
std::move(result),
LiteWalletAppRefreshOrchestrationIssue::RefreshInProgress,
"lite app refresh is already in progress");
}
if (!schedule.refreshDue && !schedule.forceRefresh) {
return skippedResult(
std::move(result),
LiteWalletAppRefreshOrchestrationIssue::RefreshNotDue,
"lite app refresh is not due");
}
result.ok = true;
result.status = LiteWalletAppRefreshOrchestrationStatus::Queued;
result.wouldQueueRefresh = true;
return result;
}
LiteWalletAppRefreshOrchestrator::LiteWalletAppRefreshOrchestrator(
LiteWalletAppRefreshOrchestratorOptions options)
: options_(options)
{
}
LiteWalletAppRefreshOrchestrationResult LiteWalletAppRefreshOrchestrator::evaluate(
const LiteWalletRefreshReadinessResult& readiness,
const LiteWalletAppRefreshScheduleInput& schedule) const
{
return orchestrateLiteWalletAppRefresh(readiness, schedule, options_);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,109 @@
#pragma once
#include "lite_wallet_refresh_readiness_policy.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletAppRefreshOrchestrationStatus {
Queued,
Skipped,
Blocked,
};
enum class LiteWalletAppRefreshScheduleTrigger {
Periodic,
Manual,
Startup,
WalletOpened,
};
enum class LiteWalletAppRefreshOrchestrationIssue {
ReadinessRejected,
UnsafeReadinessReport,
SchedulerDisabled,
RefreshNotDue,
RefreshAlreadyQueued,
RefreshInProgress,
QueuePressure,
};
struct LiteWalletAppRefreshScheduleInput {
LiteWalletAppRefreshScheduleTrigger trigger = LiteWalletAppRefreshScheduleTrigger::Periodic;
bool schedulerEnabled = true;
bool refreshDue = false;
bool forceRefresh = false;
bool refreshAlreadyQueued = false;
bool refreshInProgress = false;
std::size_t queueDepth = 0;
std::size_t maxQueueDepth = 0;
};
struct LiteWalletAppRefreshOrchestratorOptions {
bool requireEligibleReadiness = true;
std::size_t maxQueueDepth = 0;
};
struct LiteWalletAppRefreshOrchestrationIssueInfo {
LiteWalletAppRefreshOrchestrationIssue issue = LiteWalletAppRefreshOrchestrationIssue::ReadinessRejected;
std::string message;
};
struct LiteWalletAppRefreshOrchestrationResult {
bool ok = false;
bool wouldQueueRefresh = false;
bool skipped = false;
bool blocked = false;
bool readinessAccepted = false;
bool schedulerAccepted = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool stateMutationRequested = false;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool schedulerEnabled = true;
bool refreshDue = false;
bool forceRefresh = false;
bool refreshAlreadyQueued = false;
bool refreshInProgress = false;
std::size_t queueDepth = 0;
std::size_t maxQueueDepth = 0;
LiteWalletAppRefreshOrchestrationStatus status = LiteWalletAppRefreshOrchestrationStatus::Blocked;
LiteWalletAppRefreshScheduleTrigger trigger = LiteWalletAppRefreshScheduleTrigger::Periodic;
LiteWalletRefreshRouteKind route = LiteWalletRefreshRouteKind::Unavailable;
LiteWalletRefreshReadinessResult readinessResult;
std::vector<LiteWalletAppRefreshOrchestrationIssueInfo> issues;
std::string error;
};
const char* liteWalletAppRefreshOrchestrationStatusName(
LiteWalletAppRefreshOrchestrationStatus status);
const char* liteWalletAppRefreshScheduleTriggerName(
LiteWalletAppRefreshScheduleTrigger trigger);
const char* liteWalletAppRefreshOrchestrationIssueName(
LiteWalletAppRefreshOrchestrationIssue issue);
LiteWalletAppRefreshOrchestrationResult orchestrateLiteWalletAppRefresh(
const LiteWalletRefreshReadinessResult& readiness,
const LiteWalletAppRefreshScheduleInput& schedule,
LiteWalletAppRefreshOrchestratorOptions options = {});
class LiteWalletAppRefreshOrchestrator {
public:
explicit LiteWalletAppRefreshOrchestrator(LiteWalletAppRefreshOrchestratorOptions options = {});
LiteWalletAppRefreshOrchestrationResult evaluate(
const LiteWalletRefreshReadinessResult& readiness,
const LiteWalletAppRefreshScheduleInput& schedule) const;
private:
LiteWalletAppRefreshOrchestratorOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,409 @@
#include "wallet/lite_wallet_gateway.h"
#include <cctype>
#include <utility>
namespace dragonx::wallet {
namespace {
std::string trimGatewayCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
WalletBackendStatus makeGatewayStatus(WalletBackendState state, std::string message)
{
WalletBackendStatus status;
status.state = state;
status.message = std::move(message);
return status;
}
WalletBackendStatus refreshStatusForRequest(const LiteWalletRefreshRequest& request)
{
if (request.haveSyncStatus) return walletStatusFromLiteSyncStatus(request.syncStatus);
return makeGatewayStatus(
WalletBackendState::Disconnected,
"lite wallet refresh bundle assembled; runtime wallet state is unchanged");
}
LiteWalletGatewayCommandResult parsedCommandResult(const LiteWalletGatewayPlan& plan)
{
LiteWalletGatewayCommandResult result;
result.ok = true;
result.attempted = true;
result.bridgeAccepted = true;
result.plan = plan;
result.parserError = LiteResultParserError::None;
result.status = makeGatewayStatus(
WalletBackendState::Disconnected,
"lite gateway response parsed into intermediate refresh model");
result.bridgeResponseRedacted = "<redacted>";
return result;
}
} // namespace
const char* liteWalletGatewayCommandName(LiteWalletGatewayCommand command)
{
switch (command) {
case LiteWalletGatewayCommand::Info: return "info";
case LiteWalletGatewayCommand::Height: return "height";
case LiteWalletGatewayCommand::Balance: return "balance";
case LiteWalletGatewayCommand::Addresses: return "addresses";
case LiteWalletGatewayCommand::Notes: return "notes";
case LiteWalletGatewayCommand::List: return "list";
}
return "unknown";
}
const char* liteWalletGatewayAvailabilityName(LiteWalletGatewayAvailability availability)
{
switch (availability) {
case LiteWalletGatewayAvailability::Ready: return "Ready";
case LiteWalletGatewayAvailability::UnsupportedBuild: return "UnsupportedBuild";
case LiteWalletGatewayAvailability::BackendUnavailable: return "BackendUnavailable";
case LiteWalletGatewayAvailability::BridgeUnavailable: return "BridgeUnavailable";
case LiteWalletGatewayAvailability::BridgeCallsDisabled: return "BridgeCallsDisabled";
case LiteWalletGatewayAvailability::NoUsableServer: return "NoUsableServer";
}
return "Unknown";
}
std::vector<LiteWalletGatewayCommand> liteWalletRefreshCommands(const LiteWalletRefreshRequest& request)
{
std::vector<LiteWalletGatewayCommand> commands;
if (request.includeInfo) commands.push_back(LiteWalletGatewayCommand::Info);
if (request.includeHeight) commands.push_back(LiteWalletGatewayCommand::Height);
if (request.includeBalance) commands.push_back(LiteWalletGatewayCommand::Balance);
if (request.includeAddresses) commands.push_back(LiteWalletGatewayCommand::Addresses);
if (request.includeNotes) commands.push_back(LiteWalletGatewayCommand::Notes);
if (request.includeTransactions) commands.push_back(LiteWalletGatewayCommand::List);
return commands;
}
LiteWalletRefreshBundle assembleLiteWalletRefreshBundle(const std::vector<LiteWalletGatewayCommandResult>& results,
const LiteWalletRefreshRequest& request)
{
LiteWalletRefreshBundle bundle;
if (request.haveSyncStatus) {
bundle.hasSyncStatus = true;
bundle.syncStatus = request.syncStatus;
}
for (const auto& result : results) {
if (!result.ok) continue;
++bundle.successfulCommandCount;
switch (result.plan.command) {
case LiteWalletGatewayCommand::Info:
bundle.hasInfo = true;
bundle.info = result.info;
break;
case LiteWalletGatewayCommand::Height:
bundle.hasHeight = true;
bundle.height = result.height;
break;
case LiteWalletGatewayCommand::Balance:
bundle.hasBalance = true;
bundle.balance = result.balance;
break;
case LiteWalletGatewayCommand::Addresses:
bundle.hasAddresses = true;
bundle.addresses = result.addresses;
break;
case LiteWalletGatewayCommand::Notes:
bundle.hasNotes = true;
bundle.notes = result.notes;
break;
case LiteWalletGatewayCommand::List:
bundle.hasTransactions = true;
bundle.transactions = result.transactions;
break;
}
}
const auto expectedCommands = liteWalletRefreshCommands(request);
bundle.complete = bundle.successfulCommandCount == expectedCommands.size();
return bundle;
}
LiteWalletGateway::LiteWalletGateway(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteWalletGatewayOptions options)
: capabilities_(capabilities),
connectionSettings_(std::move(connectionSettings)),
bridge_(std::move(bridge)),
options_(options)
{
}
LiteWalletGatewayAvailability LiteWalletGateway::availability() const
{
if (!isLiteBuild(capabilities_)) return LiteWalletGatewayAvailability::UnsupportedBuild;
if (!supportsLiteBackend(capabilities_)) return LiteWalletGatewayAvailability::BackendUnavailable;
if (!bridge_.available()) return LiteWalletGatewayAvailability::BridgeUnavailable;
if (!selectLiteServer(connectionSettings_).ok) return LiteWalletGatewayAvailability::NoUsableServer;
if (!options_.allowBridgeCalls) return LiteWalletGatewayAvailability::BridgeCallsDisabled;
return LiteWalletGatewayAvailability::Ready;
}
WalletBackendStatus LiteWalletGateway::status() const
{
const auto currentAvailability = availability();
if (currentAvailability == LiteWalletGatewayAvailability::NoUsableServer) {
return statusFor(currentAvailability, selectLiteServer(connectionSettings_).error);
}
return statusFor(currentAvailability);
}
LiteWalletGatewayPlan LiteWalletGateway::planCommand(const LiteWalletGatewayRequest& request) const
{
LiteWalletGatewayPlan plan;
plan.command = request.command;
plan.commandName = liteWalletGatewayCommandName(request.command);
plan.bridgeExecutionAllowed = options_.allowBridgeCalls;
auto selection = selectServerForRequest(request.serverUrl);
if (!selection.ok) {
plan.error = selection.error;
return plan;
}
plan.ok = true;
plan.server = selection.server;
plan.serverIndex = selection.serverIndex;
plan.customServer = selection.customServer;
return plan;
}
LiteWalletRefreshPlan LiteWalletGateway::planRefresh(const LiteWalletRefreshRequest& request) const
{
LiteWalletRefreshPlan plan;
const auto commands = liteWalletRefreshCommands(request);
if (commands.empty()) {
plan.error = "lite refresh request has no commands";
return plan;
}
for (const auto command : commands) {
const auto commandPlan = planCommand(LiteWalletGatewayRequest{command, request.serverUrl});
if (!commandPlan.ok) {
plan.error = commandPlan.error;
return plan;
}
plan.commands.push_back(commandPlan);
}
plan.ok = true;
return plan;
}
LiteWalletGatewayCommandResult LiteWalletGateway::fetchCommand(const LiteWalletGatewayRequest& request)
{
const auto plan = planCommand(request);
if (!plan.ok) return blockedCommandResult(plan, makeGatewayStatus(WalletBackendState::Error, plan.error));
if (availability() != LiteWalletGatewayAvailability::Ready) return blockedCommandResult(plan, status());
return executePlannedCommand(plan);
}
LiteWalletRefreshResult LiteWalletGateway::refresh(const LiteWalletRefreshRequest& request)
{
LiteWalletRefreshResult result;
result.plan = planRefresh(request);
if (!result.plan.ok) {
result.status = makeGatewayStatus(WalletBackendState::Error, result.plan.error);
result.error = result.status.message;
return result;
}
if (availability() != LiteWalletGatewayAvailability::Ready) {
result.status = status();
result.error = result.status.message;
return result;
}
result.attempted = true;
for (const auto& commandPlan : result.plan.commands) {
auto commandResult = executePlannedCommand(commandPlan);
if (!commandResult.ok) {
result.status = commandResult.status;
result.error = commandResult.error;
result.commandResults.push_back(std::move(commandResult));
return result;
}
result.commandResults.push_back(std::move(commandResult));
}
result.bundle = assembleLiteWalletRefreshBundle(result.commandResults, request);
result.ok = result.bundle.complete;
result.status = refreshStatusForRequest(request);
if (!result.ok) {
result.status = makeGatewayStatus(WalletBackendState::Error, "lite refresh bundle is incomplete");
result.error = result.status.message;
}
return result;
}
LiteServerSelectionResult LiteWalletGateway::selectServerForRequest(const std::string& serverUrl) const
{
const auto overrideUrl = trimGatewayCopy(serverUrl);
if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite gateway server URL is not usable"};
}
return LiteServerSelectionResult{
true,
LiteServerEndpoint{overrideUrl, "Request", true},
0,
true,
{}
};
}
return selectLiteServer(connectionSettings_);
}
WalletBackendStatus LiteWalletGateway::statusFor(LiteWalletGatewayAvailability availability,
const std::string& detail) const
{
switch (availability) {
case LiteWalletGatewayAvailability::Ready:
return makeGatewayStatus(
WalletBackendState::Disconnected,
detail.empty() ? "lite wallet gateway scaffold ready; refresh execution is fake-test only" : detail);
case LiteWalletGatewayAvailability::UnsupportedBuild:
return makeGatewayStatus(
WalletBackendState::Unavailable,
"lite wallet gateway is unsupported in full-node builds");
case LiteWalletGatewayAvailability::BackendUnavailable:
return makeGatewayStatus(
WalletBackendState::Unavailable,
"lite backend is not linked");
case LiteWalletGatewayAvailability::BridgeUnavailable:
return makeGatewayStatus(
WalletBackendState::Unavailable,
detail.empty() ? bridge_.unavailableReason() : detail);
case LiteWalletGatewayAvailability::BridgeCallsDisabled:
return makeGatewayStatus(
WalletBackendState::Unavailable,
"lite wallet gateway bridge calls are disabled");
case LiteWalletGatewayAvailability::NoUsableServer:
return makeGatewayStatus(
WalletBackendState::Error,
detail.empty() ? "no usable lite servers are configured" : detail);
}
return makeGatewayStatus(WalletBackendState::Unavailable, "unknown lite wallet gateway state");
}
LiteWalletGatewayCommandResult LiteWalletGateway::blockedCommandResult(
const LiteWalletGatewayPlan& plan,
const WalletBackendStatus& blockedStatus) const
{
LiteWalletGatewayCommandResult result;
result.plan = plan;
result.status = blockedStatus;
result.error = blockedStatus.message;
return result;
}
LiteWalletGatewayCommandResult LiteWalletGateway::executePlannedCommand(const LiteWalletGatewayPlan& plan)
{
LiteWalletGatewayCommandResult result;
result.plan = plan;
result.attempted = true;
const auto bridgeCall = bridge_.execute(plan.commandName, plan.args);
result.bridgeResponseRedacted = bridgeCall.ok || !bridgeCall.error.empty() ? "<redacted>" : "<empty>";
if (!bridgeCall.ok) {
result.status = makeGatewayStatus(WalletBackendState::Error, "lite wallet gateway bridge call failed");
result.error = result.status.message;
return result;
}
return parseBridgeResponse(plan, bridgeCall.value);
}
LiteWalletGatewayCommandResult LiteWalletGateway::parseBridgeResponse(
const LiteWalletGatewayPlan& plan,
const std::string& response) const
{
auto result = parsedCommandResult(plan);
switch (plan.command) {
case LiteWalletGatewayCommand::Info: {
const auto parsed = parseLiteInfoResponse(response);
if (!parsed.ok) {
result.ok = false;
result.parserError = parsed.error;
break;
}
result.info = parsed.info;
return result;
}
case LiteWalletGatewayCommand::Height: {
const auto parsed = parseLiteHeightResponse(response);
if (!parsed.ok) {
result.ok = false;
result.parserError = parsed.error;
break;
}
result.height = parsed.height;
return result;
}
case LiteWalletGatewayCommand::Balance: {
const auto parsed = parseLiteBalanceResponse(response);
if (!parsed.ok) {
result.ok = false;
result.parserError = parsed.error;
break;
}
result.balance = parsed.balance;
return result;
}
case LiteWalletGatewayCommand::Addresses: {
const auto parsed = parseLiteAddressesResponse(response);
if (!parsed.ok) {
result.ok = false;
result.parserError = parsed.error;
break;
}
result.addresses = parsed.addresses;
return result;
}
case LiteWalletGatewayCommand::Notes: {
const auto parsed = parseLiteNotesResponse(response);
if (!parsed.ok) {
result.ok = false;
result.parserError = parsed.error;
break;
}
result.notes = parsed.notes;
return result;
}
case LiteWalletGatewayCommand::List: {
const auto parsed = parseLiteTransactionsResponse(response);
if (!parsed.ok) {
result.ok = false;
result.parserError = parsed.error;
break;
}
result.transactions = parsed.transactions;
return result;
}
}
result.bridgeAccepted = true;
result.status = makeGatewayStatus(WalletBackendState::Error, "lite wallet gateway response could not be parsed");
result.error = result.status.message;
result.bridgeResponseRedacted = "<redacted>";
return result;
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,163 @@
#pragma once
#include "lite_client_bridge.h"
#include "lite_connection_service.h"
#include "lite_result_parsers.h"
#include "lite_sync_service.h"
#include "wallet_backend.h"
#include "wallet_capabilities.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletGatewayCommand {
Info,
Height,
Balance,
Addresses,
Notes,
List,
};
enum class LiteWalletGatewayAvailability {
Ready,
UnsupportedBuild,
BackendUnavailable,
BridgeUnavailable,
BridgeCallsDisabled,
NoUsableServer,
};
struct LiteWalletGatewayRequest {
LiteWalletGatewayCommand command = LiteWalletGatewayCommand::Info;
std::string serverUrl;
};
struct LiteWalletGatewayPlan {
bool ok = false;
LiteWalletGatewayCommand command = LiteWalletGatewayCommand::Info;
LiteServerEndpoint server;
std::size_t serverIndex = 0;
bool customServer = false;
bool bridgeExecutionAllowed = false;
std::string commandName;
std::string args;
std::string error;
};
struct LiteWalletGatewayCommandResult {
bool ok = false;
bool attempted = false;
bool bridgeAccepted = false;
LiteWalletGatewayPlan plan;
LiteResultParserError parserError = LiteResultParserError::None;
WalletBackendStatus status;
std::string bridgeResponseRedacted = "<empty>";
std::string error;
LiteInfoResponse info;
LiteHeightResponse height;
LiteBalanceResponse balance;
LiteAddressesResponse addresses;
LiteNotesResponse notes;
LiteTransactionsResponse transactions;
};
struct LiteWalletRefreshRequest {
std::string serverUrl;
bool includeInfo = true;
bool includeHeight = true;
bool includeBalance = true;
bool includeAddresses = true;
bool includeNotes = true;
bool includeTransactions = true;
bool haveSyncStatus = false;
LiteSyncStatusResponse syncStatus;
};
struct LiteWalletRefreshPlan {
bool ok = false;
std::vector<LiteWalletGatewayPlan> commands;
std::string error;
};
struct LiteWalletRefreshBundle {
bool hasInfo = false;
bool hasHeight = false;
bool hasBalance = false;
bool hasAddresses = false;
bool hasNotes = false;
bool hasTransactions = false;
bool hasSyncStatus = false;
bool complete = false;
std::size_t successfulCommandCount = 0;
LiteInfoResponse info;
LiteHeightResponse height;
LiteBalanceResponse balance;
LiteAddressesResponse addresses;
LiteNotesResponse notes;
LiteTransactionsResponse transactions;
LiteSyncStatusResponse syncStatus;
};
struct LiteWalletRefreshResult {
bool ok = false;
bool attempted = false;
LiteWalletRefreshPlan plan;
std::vector<LiteWalletGatewayCommandResult> commandResults;
LiteWalletRefreshBundle bundle;
WalletBackendStatus status;
std::string error;
};
struct LiteWalletGatewayOptions {
bool allowBridgeCalls = false;
};
const char* liteWalletGatewayCommandName(LiteWalletGatewayCommand command);
const char* liteWalletGatewayAvailabilityName(LiteWalletGatewayAvailability availability);
std::vector<LiteWalletGatewayCommand> liteWalletRefreshCommands(const LiteWalletRefreshRequest& request);
LiteWalletRefreshBundle assembleLiteWalletRefreshBundle(const std::vector<LiteWalletGatewayCommandResult>& results,
const LiteWalletRefreshRequest& request);
class LiteWalletGateway {
public:
LiteWalletGateway(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteWalletGatewayOptions options = {});
const WalletCapabilities& capabilities() const { return capabilities_; }
const LiteConnectionSettings& connectionSettings() const { return connectionSettings_; }
const LiteWalletGatewayOptions& options() const { return options_; }
LiteWalletGatewayAvailability availability() const;
WalletBackendStatus status() const;
LiteWalletGatewayPlan planCommand(const LiteWalletGatewayRequest& request) const;
LiteWalletRefreshPlan planRefresh(const LiteWalletRefreshRequest& request) const;
LiteWalletGatewayCommandResult fetchCommand(const LiteWalletGatewayRequest& request);
LiteWalletRefreshResult refresh(const LiteWalletRefreshRequest& request);
private:
LiteServerSelectionResult selectServerForRequest(const std::string& serverUrl) const;
WalletBackendStatus statusFor(LiteWalletGatewayAvailability availability,
const std::string& detail = {}) const;
LiteWalletGatewayCommandResult blockedCommandResult(const LiteWalletGatewayPlan& plan,
const WalletBackendStatus& blockedStatus) const;
LiteWalletGatewayCommandResult executePlannedCommand(const LiteWalletGatewayPlan& plan);
LiteWalletGatewayCommandResult parseBridgeResponse(const LiteWalletGatewayPlan& plan,
const std::string& response) const;
WalletCapabilities capabilities_;
LiteConnectionSettings connectionSettings_;
LiteClientBridge bridge_;
LiteWalletGatewayOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,397 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "lite_wallet_lifecycle_service.h"
#include <cctype>
#include <utility>
#include <nlohmann/json.hpp>
namespace dragonx {
namespace wallet {
namespace {
std::string trimLifecycleCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
LiteRedactedPrivateData redactedField(LitePrivateDataKind kind, const std::string& value)
{
return LiteRedactedPrivateData{kind, !value.empty(), redactLitePrivateDataValue(value)};
}
std::vector<LiteRedactedPrivateData> createPrivateData(const LiteWalletCreateRequest& request)
{
return {redactedField(LitePrivateDataKind::Passphrase, request.passphrase)};
}
std::vector<LiteRedactedPrivateData> openPrivateData(const LiteWalletOpenRequest& request)
{
return {
redactedField(LitePrivateDataKind::WalletPath, request.walletPath),
redactedField(LitePrivateDataKind::Passphrase, request.passphrase)
};
}
std::vector<LiteRedactedPrivateData> restorePrivateData(const LiteWalletRestoreRequest& request)
{
return {
redactedField(LitePrivateDataKind::SeedPhrase, request.seedPhrase),
redactedField(LitePrivateDataKind::WalletPath, request.walletPath),
redactedField(LitePrivateDataKind::Passphrase, request.passphrase)
};
}
WalletBackendStatus lifecycleCompletedStatus()
{
return WalletBackendStatus{
WalletBackendState::Disconnected,
"lite wallet lifecycle bridge call completed; sync is not implemented",
{},
{},
0.0
};
}
} // namespace
const char* liteWalletLifecycleOperationName(LiteWalletLifecycleOperation operation)
{
switch (operation) {
case LiteWalletLifecycleOperation::CreateNew:
return "CreateNew";
case LiteWalletLifecycleOperation::OpenExisting:
return "OpenExisting";
case LiteWalletLifecycleOperation::RestoreFromSeed:
return "RestoreFromSeed";
}
return "Unknown";
}
const char* liteWalletLifecycleAvailabilityName(LiteWalletLifecycleAvailability availability)
{
switch (availability) {
case LiteWalletLifecycleAvailability::Ready:
return "Ready";
case LiteWalletLifecycleAvailability::UnsupportedBuild:
return "UnsupportedBuild";
case LiteWalletLifecycleAvailability::BackendUnavailable:
return "BackendUnavailable";
case LiteWalletLifecycleAvailability::BridgeUnavailable:
return "BridgeUnavailable";
case LiteWalletLifecycleAvailability::BridgeCallsDisabled:
return "BridgeCallsDisabled";
case LiteWalletLifecycleAvailability::NoUsableServer:
return "NoUsableServer";
}
return "Unknown";
}
const char* litePrivateDataKindName(LitePrivateDataKind kind)
{
switch (kind) {
case LitePrivateDataKind::SeedPhrase:
return "SeedPhrase";
case LitePrivateDataKind::Passphrase:
return "Passphrase";
case LitePrivateDataKind::WalletPath:
return "WalletPath";
case LitePrivateDataKind::BridgeResponse:
return "BridgeResponse";
}
return "Unknown";
}
std::string redactLitePrivateDataValue(const std::string& value)
{
return value.empty() ? "<empty>" : "<redacted>";
}
LiteWalletLifecycleService::LiteWalletLifecycleService(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteWalletLifecycleOptions options)
: capabilities_(capabilities),
connectionSettings_(std::move(connectionSettings)),
bridge_(std::move(bridge)),
options_(options)
{
}
LiteWalletLifecycleAvailability LiteWalletLifecycleService::availability() const
{
if (!isLiteBuild(capabilities_)) return LiteWalletLifecycleAvailability::UnsupportedBuild;
if (!supportsLiteBackend(capabilities_)) return LiteWalletLifecycleAvailability::BackendUnavailable;
if (!bridge_.available()) return LiteWalletLifecycleAvailability::BridgeUnavailable;
if (!selectLiteServer(connectionSettings_).ok) return LiteWalletLifecycleAvailability::NoUsableServer;
if (!options_.allowBridgeCalls) return LiteWalletLifecycleAvailability::BridgeCallsDisabled;
return LiteWalletLifecycleAvailability::Ready;
}
WalletBackendStatus LiteWalletLifecycleService::status() const
{
const auto currentAvailability = availability();
if (currentAvailability == LiteWalletLifecycleAvailability::NoUsableServer) {
return statusFor(currentAvailability, selectLiteServer(connectionSettings_).error);
}
return statusFor(currentAvailability);
}
LiteWalletLifecyclePlan LiteWalletLifecycleService::planCreateWallet(
const LiteWalletCreateRequest& request) const
{
return makePlan(LiteWalletLifecycleOperation::CreateNew,
request.serverUrl,
createPrivateData(request));
}
LiteWalletLifecyclePlan LiteWalletLifecycleService::planOpenWallet(
const LiteWalletOpenRequest& request) const
{
return makePlan(LiteWalletLifecycleOperation::OpenExisting,
request.serverUrl,
openPrivateData(request));
}
LiteWalletLifecyclePlan LiteWalletLifecycleService::planRestoreWallet(
const LiteWalletRestoreRequest& request) const
{
const std::string validationError = trimLifecycleCopy(request.seedPhrase).empty()
? "restore seed phrase is required"
: std::string();
return makePlan(LiteWalletLifecycleOperation::RestoreFromSeed,
request.serverUrl,
restorePrivateData(request),
validationError);
}
LiteWalletLifecycleResult LiteWalletLifecycleService::createWallet(
const LiteWalletCreateRequest& request)
{
const auto plan = planCreateWallet(request);
if (!plan.ok) return blockedResult(plan, WalletBackendStatus{WalletBackendState::Error, plan.error, {}, {}, 0.0});
const auto currentStatus = status();
if (availability() != LiteWalletLifecycleAvailability::Ready) return blockedResult(plan, currentStatus);
return executeCreate(request, plan);
}
LiteWalletLifecycleResult LiteWalletLifecycleService::openWallet(
const LiteWalletOpenRequest& request)
{
const auto plan = planOpenWallet(request);
if (!plan.ok) return blockedResult(plan, WalletBackendStatus{WalletBackendState::Error, plan.error, {}, {}, 0.0});
const auto currentStatus = status();
if (availability() != LiteWalletLifecycleAvailability::Ready) return blockedResult(plan, currentStatus);
return executeOpen(request, plan);
}
LiteWalletLifecycleResult LiteWalletLifecycleService::restoreWallet(
const LiteWalletRestoreRequest& request)
{
const auto plan = planRestoreWallet(request);
if (!plan.ok) return blockedResult(plan, WalletBackendStatus{WalletBackendState::Error, plan.error, {}, {}, 0.0});
const auto currentStatus = status();
if (availability() != LiteWalletLifecycleAvailability::Ready) return blockedResult(plan, currentStatus);
return executeRestore(request, plan);
}
LiteServerSelectionResult LiteWalletLifecycleService::selectServerForRequest(
const std::string& serverUrl) const
{
const std::string overrideUrl = trimLifecycleCopy(serverUrl);
if (!overrideUrl.empty()) {
if (!isLiteServerUrlUsable(overrideUrl)) {
return LiteServerSelectionResult{false, {}, 0, false, "lite lifecycle server URL is not usable"};
}
return LiteServerSelectionResult{
true,
LiteServerEndpoint{overrideUrl, "Request", true},
0,
true,
{}
};
}
return selectLiteServer(connectionSettings_);
}
LiteWalletLifecyclePlan LiteWalletLifecycleService::makePlan(
LiteWalletLifecycleOperation operation,
const std::string& serverUrl,
std::vector<LiteRedactedPrivateData> privateData,
const std::string& validationError) const
{
LiteWalletLifecyclePlan plan;
plan.operation = operation;
plan.privateData = std::move(privateData);
plan.bridgeExecutionAllowed = options_.allowBridgeCalls;
if (!validationError.empty()) {
plan.error = validationError;
return plan;
}
auto selection = selectServerForRequest(serverUrl);
if (!selection.ok) {
plan.error = selection.error;
return plan;
}
plan.ok = true;
plan.server = selection.server;
plan.serverIndex = selection.serverIndex;
plan.customServer = selection.customServer;
return plan;
}
WalletBackendStatus LiteWalletLifecycleService::statusFor(
LiteWalletLifecycleAvailability availability,
const std::string& detail) const
{
switch (availability) {
case LiteWalletLifecycleAvailability::Ready:
return WalletBackendStatus{
WalletBackendState::Disconnected,
detail.empty() ? "lite wallet lifecycle scaffold ready; sync is not implemented" : detail,
{},
{},
0.0
};
case LiteWalletLifecycleAvailability::UnsupportedBuild:
return WalletBackendStatus{
WalletBackendState::Unavailable,
"lite wallet lifecycle is unsupported in full-node builds",
{},
{},
0.0
};
case LiteWalletLifecycleAvailability::BackendUnavailable:
return WalletBackendStatus{
WalletBackendState::Unavailable,
"lite backend is not linked",
{},
{},
0.0
};
case LiteWalletLifecycleAvailability::BridgeUnavailable:
return WalletBackendStatus{
WalletBackendState::Unavailable,
detail.empty() ? bridge_.unavailableReason() : detail,
{},
{},
0.0
};
case LiteWalletLifecycleAvailability::BridgeCallsDisabled:
return WalletBackendStatus{
WalletBackendState::Unavailable,
"lite wallet lifecycle bridge calls are disabled",
{},
{},
0.0
};
case LiteWalletLifecycleAvailability::NoUsableServer:
return WalletBackendStatus{
WalletBackendState::Error,
detail.empty() ? "no usable lite servers are configured" : detail,
{},
{},
0.0
};
}
return WalletBackendStatus{WalletBackendState::Unavailable, "unknown lite wallet lifecycle state", {}, {}, 0.0};
}
LiteWalletLifecycleResult LiteWalletLifecycleService::executeCreate(
const LiteWalletCreateRequest& request,
const LiteWalletLifecyclePlan& plan)
{
auto bridgeCall = bridge_.initializeNew(request.dangerous, plan.server.url);
return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall);
}
LiteWalletLifecycleResult LiteWalletLifecycleService::executeOpen(
const LiteWalletOpenRequest& request,
const LiteWalletLifecyclePlan& plan)
{
auto bridgeCall = bridge_.initializeExisting(request.dangerous, plan.server.url);
return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall);
}
LiteWalletLifecycleResult LiteWalletLifecycleService::executeRestore(
const LiteWalletRestoreRequest& request,
const LiteWalletLifecyclePlan& plan)
{
auto bridgeCall = bridge_.initializeNewFromPhrase(
request.dangerous,
plan.server.url,
request.seedPhrase,
request.birthday,
request.account,
request.overwrite);
return bridgeResult(plan, lifecycleCompletedStatus(), bridgeCall);
}
LiteWalletLifecycleResult LiteWalletLifecycleService::blockedResult(
const LiteWalletLifecyclePlan& plan,
const WalletBackendStatus& blockedStatus) const
{
LiteWalletLifecycleResult result;
result.operation = plan.operation;
result.plan = plan;
result.status = blockedStatus;
result.error = blockedStatus.message;
return result;
}
LiteWalletLifecycleResult LiteWalletLifecycleService::bridgeResult(
const LiteWalletLifecyclePlan& plan,
const WalletBackendStatus& successStatus,
const LiteBridgeStringResult& bridgeCall) const
{
LiteWalletLifecycleResult result;
result.operation = plan.operation;
result.plan = plan;
result.attempted = true;
result.bridgeResponseRedacted = redactLitePrivateDataValue(bridgeCall.ok ? bridgeCall.value : bridgeCall.error);
if (!bridgeCall.ok) {
result.status = WalletBackendStatus{
WalletBackendState::Error,
"lite wallet lifecycle bridge call failed",
{},
{},
0.0
};
result.error = result.status.message;
return result;
}
result.ok = true;
result.bridgeAccepted = true;
// The bridge already classifies "Error:"-prefixed responses as failures (ok=false).
// A successful init returns a JSON document (seed info for create, wallet info for
// open/restore); treat a well-formed JSON response as a genuinely ready wallet rather
// than discarding it. (Richer field extraction lands with the sync slice.)
result.walletReady = nlohmann::json::accept(bridgeCall.value);
result.status = successStatus;
return result;
}
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,152 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "lite_client_bridge.h"
#include "lite_connection_service.h"
#include "wallet_backend.h"
#include "wallet_capabilities.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx {
namespace wallet {
enum class LiteWalletLifecycleOperation {
CreateNew,
OpenExisting,
RestoreFromSeed
};
enum class LiteWalletLifecycleAvailability {
Ready,
UnsupportedBuild,
BackendUnavailable,
BridgeUnavailable,
BridgeCallsDisabled,
NoUsableServer
};
enum class LitePrivateDataKind {
SeedPhrase,
Passphrase,
WalletPath,
BridgeResponse
};
struct LiteRedactedPrivateData {
LitePrivateDataKind kind = LitePrivateDataKind::SeedPhrase;
bool present = false;
std::string redactedValue = "<empty>";
};
struct LiteWalletCreateRequest {
bool dangerous = false;
std::string serverUrl;
std::string passphrase;
};
struct LiteWalletOpenRequest {
bool dangerous = false;
std::string serverUrl;
std::string walletPath;
std::string passphrase;
};
struct LiteWalletRestoreRequest {
bool dangerous = false;
std::string serverUrl;
std::string seedPhrase;
unsigned long long birthday = 0;
unsigned long long account = 0;
bool overwrite = false;
std::string walletPath;
std::string passphrase;
};
struct LiteWalletLifecyclePlan {
bool ok = false;
LiteWalletLifecycleOperation operation = LiteWalletLifecycleOperation::CreateNew;
LiteServerEndpoint server;
std::size_t serverIndex = 0;
bool customServer = false;
bool bridgeExecutionAllowed = false;
std::vector<LiteRedactedPrivateData> privateData;
std::string error;
};
struct LiteWalletLifecycleResult {
bool ok = false;
bool attempted = false;
bool bridgeAccepted = false;
bool walletReady = false;
LiteWalletLifecycleOperation operation = LiteWalletLifecycleOperation::CreateNew;
LiteWalletLifecyclePlan plan;
WalletBackendStatus status;
std::string bridgeResponseRedacted = "<empty>";
std::string error;
};
struct LiteWalletLifecycleOptions {
bool allowBridgeCalls = false;
};
const char* liteWalletLifecycleOperationName(LiteWalletLifecycleOperation operation);
const char* liteWalletLifecycleAvailabilityName(LiteWalletLifecycleAvailability availability);
const char* litePrivateDataKindName(LitePrivateDataKind kind);
std::string redactLitePrivateDataValue(const std::string& value);
class LiteWalletLifecycleService {
public:
LiteWalletLifecycleService(WalletCapabilities capabilities,
LiteConnectionSettings connectionSettings,
LiteClientBridge bridge,
LiteWalletLifecycleOptions options = {});
const LiteConnectionSettings& connectionSettings() const { return connectionSettings_; }
const WalletCapabilities& capabilities() const { return capabilities_; }
const LiteWalletLifecycleOptions& options() const { return options_; }
LiteWalletLifecycleAvailability availability() const;
WalletBackendStatus status() const;
LiteWalletLifecyclePlan planCreateWallet(const LiteWalletCreateRequest& request) const;
LiteWalletLifecyclePlan planOpenWallet(const LiteWalletOpenRequest& request) const;
LiteWalletLifecyclePlan planRestoreWallet(const LiteWalletRestoreRequest& request) const;
LiteWalletLifecycleResult createWallet(const LiteWalletCreateRequest& request);
LiteWalletLifecycleResult openWallet(const LiteWalletOpenRequest& request);
LiteWalletLifecycleResult restoreWallet(const LiteWalletRestoreRequest& request);
private:
LiteServerSelectionResult selectServerForRequest(const std::string& serverUrl) const;
LiteWalletLifecyclePlan makePlan(LiteWalletLifecycleOperation operation,
const std::string& serverUrl,
std::vector<LiteRedactedPrivateData> privateData,
const std::string& validationError = {}) const;
WalletBackendStatus statusFor(LiteWalletLifecycleAvailability availability,
const std::string& detail = {}) const;
LiteWalletLifecycleResult executeCreate(const LiteWalletCreateRequest& request,
const LiteWalletLifecyclePlan& plan);
LiteWalletLifecycleResult executeOpen(const LiteWalletOpenRequest& request,
const LiteWalletLifecyclePlan& plan);
LiteWalletLifecycleResult executeRestore(const LiteWalletRestoreRequest& request,
const LiteWalletLifecyclePlan& plan);
LiteWalletLifecycleResult blockedResult(const LiteWalletLifecyclePlan& plan,
const WalletBackendStatus& blockedStatus) const;
LiteWalletLifecycleResult bridgeResult(const LiteWalletLifecyclePlan& plan,
const WalletBackendStatus& successStatus,
const LiteBridgeStringResult& bridgeCall) const;
WalletCapabilities capabilities_;
LiteConnectionSettings connectionSettings_;
LiteClientBridge bridge_;
LiteWalletLifecycleOptions options_;
};
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,440 @@
#include "lite_wallet_lifecycle_ui_adapter.h"
#include <sstream>
#include <utility>
namespace dragonx {
namespace wallet {
namespace {
void addIssue(std::vector<LiteWalletLifecycleUiExecutionIssueInfo>& issues,
LiteWalletLifecycleUiExecutionIssue issue,
std::string message)
{
issues.push_back(LiteWalletLifecycleUiExecutionIssueInfo{issue, std::move(message)});
}
LiteWalletLifecycleUiExecutionResult stoppedResult(
LiteWalletLifecycleUiExecutionResult result,
LiteWalletLifecycleUiExecutionStatus status,
LiteWalletLifecycleUiExecutionIssue issue,
const std::string& message)
{
result.status = status;
result.error = message;
result.ok = false;
addIssue(result.issues, issue, message);
return result;
}
LiteWalletLifecycleUiExecutionStatus statusFromServerSelection(
LiteWalletServerSelectionUiExecutionStatus status)
{
switch (status) {
case LiteWalletServerSelectionUiExecutionStatus::ReadyForFutureLifecycle:
return LiteWalletLifecycleUiExecutionStatus::ReadyForFutureLifecycleRequest;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForLiteBuild:
return LiteWalletLifecycleUiExecutionStatus::WaitingForLiteBuild;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForBackendCapability:
return LiteWalletLifecycleUiExecutionStatus::WaitingForBackendCapability;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings:
return LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection:
return LiteWalletLifecycleUiExecutionStatus::WaitingForServerSelection;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForPersistenceOwner:
return LiteWalletLifecycleUiExecutionStatus::WaitingForPersistedServerSelection;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForDisplayStatus:
return LiteWalletLifecycleUiExecutionStatus::WaitingForDisplayStatus;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForLifecycleUi:
return LiteWalletLifecycleUiExecutionStatus::WaitingForLifecycleUi;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForPrivateDataRedaction:
return LiteWalletLifecycleUiExecutionStatus::WaitingForPrivateDataRedaction;
case LiteWalletServerSelectionUiExecutionStatus::WaitingForSyncPlannerFeed:
return LiteWalletLifecycleUiExecutionStatus::WaitingForSyncPlannerFeed;
case LiteWalletServerSelectionUiExecutionStatus::SettingsSaveFailed:
return LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
case LiteWalletServerSelectionUiExecutionStatus::RuntimeExecutionDisabled:
return LiteWalletLifecycleUiExecutionStatus::RuntimeExecutionDisabled;
}
return LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
}
LiteWalletLifecycleUiExecutionStatus statusFromLifecycle(
LiteWalletServerLifecycleReadinessStatus status)
{
switch (status) {
case LiteWalletServerLifecycleReadinessStatus::ReadyForFutureLifecycle:
return LiteWalletLifecycleUiExecutionStatus::ReadyForFutureLifecycleRequest;
case LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild:
return LiteWalletLifecycleUiExecutionStatus::WaitingForLiteBuild;
case LiteWalletServerLifecycleReadinessStatus::WaitingForBackendCapability:
return LiteWalletLifecycleUiExecutionStatus::WaitingForBackendCapability;
case LiteWalletServerLifecycleReadinessStatus::WaitingForServerSelection:
return LiteWalletLifecycleUiExecutionStatus::WaitingForServerSelection;
case LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent:
return LiteWalletLifecycleUiExecutionStatus::WaitingForPersistedServerSelection;
case LiteWalletServerLifecycleReadinessStatus::WaitingForDisplayStatus:
return LiteWalletLifecycleUiExecutionStatus::WaitingForDisplayStatus;
case LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi:
return LiteWalletLifecycleUiExecutionStatus::WaitingForLifecycleUi;
case LiteWalletServerLifecycleReadinessStatus::WaitingForPrivateDataRedaction:
return LiteWalletLifecycleUiExecutionStatus::WaitingForPrivateDataRedaction;
case LiteWalletServerLifecycleReadinessStatus::WaitingForSyncPlannerFeed:
return LiteWalletLifecycleUiExecutionStatus::WaitingForSyncPlannerFeed;
case LiteWalletServerLifecycleReadinessStatus::RuntimeExecutionDisabled:
return LiteWalletLifecycleUiExecutionStatus::RuntimeExecutionDisabled;
}
return LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
}
LiteWalletLifecycleUiExecutionIssue issueFromLifecycle(
LiteWalletServerLifecycleReadinessIssue issue)
{
switch (issue) {
case LiteWalletServerLifecycleReadinessIssue::FullNodeBuild:
return LiteWalletLifecycleUiExecutionIssue::FullNodeBuild;
case LiteWalletServerLifecycleReadinessIssue::LiteBackendCapabilityMissing:
return LiteWalletLifecycleUiExecutionIssue::LiteBackendCapabilityMissing;
case LiteWalletServerLifecycleReadinessIssue::PersistedSettingsNotLoaded:
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
case LiteWalletServerLifecycleReadinessIssue::PersistedServerSelectionIntentMissing:
return LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing;
case LiteWalletServerLifecycleReadinessIssue::ServerSelectionMissing:
return LiteWalletLifecycleUiExecutionIssue::ServerSelectionMissing;
case LiteWalletServerLifecycleReadinessIssue::ServerPersistenceOwnerMissing:
return LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing;
case LiteWalletServerLifecycleReadinessIssue::SelectedServerDisplayMissing:
return LiteWalletLifecycleUiExecutionIssue::SelectedServerDisplayMissing;
case LiteWalletServerLifecycleReadinessIssue::LifecycleUiOwnerMissing:
return LiteWalletLifecycleUiExecutionIssue::LifecycleUiOwnerMissing;
case LiteWalletServerLifecycleReadinessIssue::LifecycleOperationUnconfirmed:
return LiteWalletLifecycleUiExecutionIssue::LifecycleOperationUnconfirmed;
case LiteWalletServerLifecycleReadinessIssue::OpenWalletPathMissing:
return LiteWalletLifecycleUiExecutionIssue::OpenWalletPathMissing;
case LiteWalletServerLifecycleReadinessIssue::RestoreSeedMissing:
return LiteWalletLifecycleUiExecutionIssue::RestoreSeedMissing;
case LiteWalletServerLifecycleReadinessIssue::PrivateDataRedactionMissing:
return LiteWalletLifecycleUiExecutionIssue::PrivateDataRedactionMissing;
case LiteWalletServerLifecycleReadinessIssue::SyncPlannerFeedMissing:
return LiteWalletLifecycleUiExecutionIssue::SyncPlannerFeedMissing;
case LiteWalletServerLifecycleReadinessIssue::RealLifecycleExecutionRequested:
return LiteWalletLifecycleUiExecutionIssue::WalletLifecycleExecutionRequested;
}
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
}
LiteWalletLifecycleUiExecutionIssue issueFromServerSelection(
LiteWalletServerSelectionUiExecutionIssue issue)
{
switch (issue) {
case LiteWalletServerSelectionUiExecutionIssue::FullNodeBuild:
return LiteWalletLifecycleUiExecutionIssue::FullNodeBuild;
case LiteWalletServerSelectionUiExecutionIssue::LiteBackendCapabilityMissing:
return LiteWalletLifecycleUiExecutionIssue::LiteBackendCapabilityMissing;
case LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded:
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
case LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing:
return LiteWalletLifecycleUiExecutionIssue::ServerSelectionMissing;
case LiteWalletServerSelectionUiExecutionIssue::ServerPersistenceOwnerMissing:
return LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing;
case LiteWalletServerSelectionUiExecutionIssue::SelectedServerDisplayMissing:
return LiteWalletLifecycleUiExecutionIssue::SelectedServerDisplayMissing;
case LiteWalletServerSelectionUiExecutionIssue::LifecycleUiOwnerMissing:
return LiteWalletLifecycleUiExecutionIssue::LifecycleUiOwnerMissing;
case LiteWalletServerSelectionUiExecutionIssue::PrivateDataRedactionMissing:
return LiteWalletLifecycleUiExecutionIssue::PrivateDataRedactionMissing;
case LiteWalletServerSelectionUiExecutionIssue::SyncPlannerFeedMissing:
return LiteWalletLifecycleUiExecutionIssue::SyncPlannerFeedMissing;
case LiteWalletServerSelectionUiExecutionIssue::SettingsSaveFailed:
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
case LiteWalletServerSelectionUiExecutionIssue::ServerConnectivityCheckRequested:
return LiteWalletLifecycleUiExecutionIssue::ServerConnectivityCheckRequested;
case LiteWalletServerSelectionUiExecutionIssue::WalletLifecycleExecutionRequested:
return LiteWalletLifecycleUiExecutionIssue::WalletLifecycleExecutionRequested;
case LiteWalletServerSelectionUiExecutionIssue::SyncRequested:
return LiteWalletLifecycleUiExecutionIssue::SyncRequested;
case LiteWalletServerSelectionUiExecutionIssue::SyncStatusPollingRequested:
return LiteWalletLifecycleUiExecutionIssue::SyncStatusPollingRequested;
case LiteWalletServerSelectionUiExecutionIssue::WorkerQueueRequested:
return LiteWalletLifecycleUiExecutionIssue::WorkerQueueRequested;
case LiteWalletServerSelectionUiExecutionIssue::WalletStateMutationRequested:
return LiteWalletLifecycleUiExecutionIssue::WalletStateMutationRequested;
case LiteWalletServerSelectionUiExecutionIssue::WalletFilePersistenceRequested:
return LiteWalletLifecycleUiExecutionIssue::WalletFilePersistenceRequested;
case LiteWalletServerSelectionUiExecutionIssue::SendImportExportRequested:
return LiteWalletLifecycleUiExecutionIssue::SendImportExportRequested;
}
return LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
}
void copyServerSelectionIssues(LiteWalletLifecycleUiExecutionResult& result)
{
for (const auto& issue : result.serverSelectionReport.issues) {
addIssue(result.issues, issueFromServerSelection(issue.issue), issue.message);
}
}
void copyLifecycleIssues(LiteWalletLifecycleUiExecutionResult& result)
{
for (const auto& issue : result.lifecycleReadiness.issues) {
addIssue(result.issues, issueFromLifecycle(issue.issue), issue.message);
}
}
LiteWalletServerSelectionUiExecutionInput makeServerSelectionInput(
const LiteWalletLifecycleUiExecutionInput& input,
const config::Settings& settings)
{
LiteWalletServerSelectionUiExecutionInput selection;
selection.capabilities = input.capabilities;
selection.persistence.settingsLoaded = input.settingsLoaded;
selection.persistence.havePersistedSelectionIntent =
!input.requirePersistedServerSelectionIntent || settings.getLitePersistSelectedServer();
selection.persistence.persistSelectedServer = false;
selection.persistence.persistenceOwnerReady = true;
selection.persistence.writeSettings = false;
selection.ui = input.ui;
selection.operation = input.request.operation;
selection.createRequest = input.request.createRequest;
selection.openRequest = input.request.openRequest;
selection.restoreRequest = input.request.restoreRequest;
selection.requireLifecycleReadiness = true;
return selection;
}
std::string buildDiagnosticSummary(const LiteWalletLifecycleUiExecutionResult& result)
{
std::ostringstream out;
out << "operation=" << liteWalletLifecycleOperationName(result.lifecycleInput.operation)
<< ";server=" << result.selectedServerUrlRedacted
<< ";request=" << result.requestSummaryRedacted
<< ";network=false;bridge=false;lifecycle=false;sync=false;wallet_state=false;wallet_file=false";
return out.str();
}
} // namespace
const char* liteWalletLifecycleUiExecutionStatusName(
LiteWalletLifecycleUiExecutionStatus status)
{
switch (status) {
case LiteWalletLifecycleUiExecutionStatus::ReadyForFutureLifecycleRequest:
return "ReadyForFutureLifecycleRequest";
case LiteWalletLifecycleUiExecutionStatus::WaitingForLiteBuild:
return "WaitingForLiteBuild";
case LiteWalletLifecycleUiExecutionStatus::WaitingForBackendCapability:
return "WaitingForBackendCapability";
case LiteWalletLifecycleUiExecutionStatus::WaitingForSettings:
return "WaitingForSettings";
case LiteWalletLifecycleUiExecutionStatus::WaitingForPersistedServerSelection:
return "WaitingForPersistedServerSelection";
case LiteWalletLifecycleUiExecutionStatus::WaitingForServerSelection:
return "WaitingForServerSelection";
case LiteWalletLifecycleUiExecutionStatus::WaitingForDisplayStatus:
return "WaitingForDisplayStatus";
case LiteWalletLifecycleUiExecutionStatus::WaitingForLifecycleUi:
return "WaitingForLifecycleUi";
case LiteWalletLifecycleUiExecutionStatus::WaitingForRequest:
return "WaitingForRequest";
case LiteWalletLifecycleUiExecutionStatus::WaitingForPrivateDataRedaction:
return "WaitingForPrivateDataRedaction";
case LiteWalletLifecycleUiExecutionStatus::WaitingForSyncPlannerFeed:
return "WaitingForSyncPlannerFeed";
case LiteWalletLifecycleUiExecutionStatus::RuntimeExecutionDisabled:
return "RuntimeExecutionDisabled";
}
return "WaitingForSettings";
}
const char* liteWalletLifecycleUiExecutionIssueName(
LiteWalletLifecycleUiExecutionIssue issue)
{
switch (issue) {
case LiteWalletLifecycleUiExecutionIssue::FullNodeBuild:
return "FullNodeBuild";
case LiteWalletLifecycleUiExecutionIssue::LiteBackendCapabilityMissing:
return "LiteBackendCapabilityMissing";
case LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded:
return "SettingsNotLoaded";
case LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing:
return "PersistedServerSelectionIntentMissing";
case LiteWalletLifecycleUiExecutionIssue::ServerSelectionMissing:
return "ServerSelectionMissing";
case LiteWalletLifecycleUiExecutionIssue::SelectedServerDisplayMissing:
return "SelectedServerDisplayMissing";
case LiteWalletLifecycleUiExecutionIssue::LifecycleUiOwnerMissing:
return "LifecycleUiOwnerMissing";
case LiteWalletLifecycleUiExecutionIssue::LifecycleOperationUnconfirmed:
return "LifecycleOperationUnconfirmed";
case LiteWalletLifecycleUiExecutionIssue::OpenWalletPathMissing:
return "OpenWalletPathMissing";
case LiteWalletLifecycleUiExecutionIssue::RestoreSeedMissing:
return "RestoreSeedMissing";
case LiteWalletLifecycleUiExecutionIssue::PrivateDataRedactionMissing:
return "PrivateDataRedactionMissing";
case LiteWalletLifecycleUiExecutionIssue::SyncPlannerFeedMissing:
return "SyncPlannerFeedMissing";
case LiteWalletLifecycleUiExecutionIssue::ServerConnectivityCheckRequested:
return "ServerConnectivityCheckRequested";
case LiteWalletLifecycleUiExecutionIssue::WalletExistsCheckRequested:
return "WalletExistsCheckRequested";
case LiteWalletLifecycleUiExecutionIssue::WalletLifecycleExecutionRequested:
return "WalletLifecycleExecutionRequested";
case LiteWalletLifecycleUiExecutionIssue::SyncRequested:
return "SyncRequested";
case LiteWalletLifecycleUiExecutionIssue::SyncStatusPollingRequested:
return "SyncStatusPollingRequested";
case LiteWalletLifecycleUiExecutionIssue::WorkerQueueRequested:
return "WorkerQueueRequested";
case LiteWalletLifecycleUiExecutionIssue::WalletStateMutationRequested:
return "WalletStateMutationRequested";
case LiteWalletLifecycleUiExecutionIssue::WalletFilePersistenceRequested:
return "WalletFilePersistenceRequested";
case LiteWalletLifecycleUiExecutionIssue::SendImportExportRequested:
return "SendImportExportRequested";
case LiteWalletLifecycleUiExecutionIssue::SettingsWriteRequested:
return "SettingsWriteRequested";
}
return "SettingsNotLoaded";
}
LiteWalletLifecycleUiExecutionResult executeLiteWalletLifecycleUiRequest(
config::Settings& settings,
const LiteWalletLifecycleUiExecutionInput& input)
{
LiteWalletLifecycleUiExecutionResult result;
auto stopForRuntime = [&](LiteWalletLifecycleUiExecutionIssue issue,
const std::string& message) {
return stoppedResult(std::move(result),
LiteWalletLifecycleUiExecutionStatus::RuntimeExecutionDisabled,
issue,
message);
};
if (input.settingsWriteRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::SettingsWriteRequested,
"lite lifecycle request adapter cannot write settings");
}
if (input.serverConnectivityCheckRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::ServerConnectivityCheckRequested,
"lite lifecycle request adapter cannot check server connectivity");
}
if (input.walletExistsCheckRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WalletExistsCheckRequested,
"lite lifecycle request adapter cannot check wallet existence");
}
if (input.walletLifecycleExecutionRequested || input.ui.realLifecycleExecutionRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WalletLifecycleExecutionRequested,
"lite lifecycle request adapter cannot execute wallet lifecycle actions");
}
if (input.syncStartRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::SyncRequested,
"lite lifecycle request adapter cannot start sync");
}
if (input.syncStatusPollingRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::SyncStatusPollingRequested,
"lite lifecycle request adapter cannot poll syncstatus");
}
if (input.workerQueueEnqueueRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WorkerQueueRequested,
"lite lifecycle request adapter cannot enqueue wallet workers");
}
if (input.walletStateMutationRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WalletStateMutationRequested,
"lite lifecycle request adapter cannot mutate WalletState");
}
if (input.walletFilePersistenceRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::WalletFilePersistenceRequested,
"lite lifecycle request adapter cannot persist wallet files");
}
if (input.sendImportExportExecutionRequested) {
return stopForRuntime(LiteWalletLifecycleUiExecutionIssue::SendImportExportRequested,
"lite lifecycle request adapter cannot execute send/import/export flows");
}
if (!input.settingsLoaded) {
return stoppedResult(std::move(result),
LiteWalletLifecycleUiExecutionStatus::WaitingForSettings,
LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded,
"lite lifecycle settings are not loaded");
}
if (!input.request.requestProvided) {
return stoppedResult(std::move(result),
LiteWalletLifecycleUiExecutionStatus::WaitingForRequest,
LiteWalletLifecycleUiExecutionIssue::LifecycleUiOwnerMissing,
"lite lifecycle request intent is missing");
}
if (input.requirePersistedServerSelectionIntent && !settings.getLitePersistSelectedServer()) {
return stoppedResult(std::move(result),
LiteWalletLifecycleUiExecutionStatus::WaitingForPersistedServerSelection,
LiteWalletLifecycleUiExecutionIssue::PersistedServerSelectionIntentMissing,
"lite lifecycle requires persisted selected-server intent");
}
result.connectionSettings = liteConnectionSettingsFromAppSettings(settings);
result.selectedServerSettingsLoaded = true;
LiteWalletServerSelectionUiExecutionInput selectionInput = makeServerSelectionInput(input, settings);
result.serverSelectionReport = executeLiteWalletServerSelectionUi(settings, selectionInput);
result.connectionSettings = result.serverSelectionReport.connectionSettings;
result.selectedServer = result.serverSelectionReport.selectedServer;
result.selectedServerUrlRedacted = result.serverSelectionReport.selectedServerUrlRedacted;
result.selectedServerIntentAccepted = result.serverSelectionReport.selectedServerIntentAccepted;
result.status = statusFromServerSelection(result.serverSelectionReport.status);
result.lifecycleInput = result.serverSelectionReport.lifecycleInput;
result.lifecycleReadiness = result.serverSelectionReport.lifecycleReadiness;
result.lifecycleReadinessAccepted = result.lifecycleReadiness.ok;
result.lifecycleReportCouldFeedSyncPlanners =
result.lifecycleReadiness.lifecycleReportCouldFeedSyncPlanners;
result.requestAccepted = result.lifecycleReadiness.requestAccepted;
result.privateDataRedacted = result.lifecycleReadiness.requestPlan.privateInputsRedacted;
result.privateData = result.lifecycleReadiness.requestPlan.privateData;
result.requestSummaryRedacted = result.lifecycleReadiness.requestPlan.requestSummary;
result.diagnosticSummary = buildDiagnosticSummary(result);
copyServerSelectionIssues(result);
copyLifecycleIssues(result);
const bool lifecycleReadinessEvaluated = result.lifecycleReadiness.ok ||
!result.lifecycleReadiness.issues.empty() ||
!result.lifecycleReadiness.error.empty() ||
result.lifecycleReadiness.serverSelectionAccepted ||
result.lifecycleReadiness.requestAccepted;
if (!result.serverSelectionReport.ok && !lifecycleReadinessEvaluated) {
result.ok = false;
result.status = statusFromServerSelection(result.serverSelectionReport.status);
result.error = result.serverSelectionReport.error;
return result;
}
if (!result.lifecycleReadiness.ok) {
result.ok = false;
result.status = statusFromLifecycle(result.lifecycleReadiness.status);
result.error = result.lifecycleReadiness.error.empty()
? result.serverSelectionReport.error
: result.lifecycleReadiness.error;
return result;
}
result.ok = true;
result.status = LiteWalletLifecycleUiExecutionStatus::ReadyForFutureLifecycleRequest;
return result;
}
LiteWalletLifecycleUiExecutionAdapter::LiteWalletLifecycleUiExecutionAdapter(
config::Settings& settings)
: settings_(settings)
{
}
LiteWalletLifecycleUiExecutionResult LiteWalletLifecycleUiExecutionAdapter::execute(
const LiteWalletLifecycleUiExecutionInput& input) const
{
return executeLiteWalletLifecycleUiRequest(settings_, input);
}
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,146 @@
#pragma once
#include "lite_wallet_server_selection_adapter.h"
#include "../config/settings.h"
#include <string>
#include <vector>
namespace dragonx {
namespace wallet {
enum class LiteWalletLifecycleUiExecutionStatus {
ReadyForFutureLifecycleRequest,
WaitingForLiteBuild,
WaitingForBackendCapability,
WaitingForSettings,
WaitingForPersistedServerSelection,
WaitingForServerSelection,
WaitingForDisplayStatus,
WaitingForLifecycleUi,
WaitingForRequest,
WaitingForPrivateDataRedaction,
WaitingForSyncPlannerFeed,
RuntimeExecutionDisabled
};
enum class LiteWalletLifecycleUiExecutionIssue {
FullNodeBuild,
LiteBackendCapabilityMissing,
SettingsNotLoaded,
PersistedServerSelectionIntentMissing,
ServerSelectionMissing,
SelectedServerDisplayMissing,
LifecycleUiOwnerMissing,
LifecycleOperationUnconfirmed,
OpenWalletPathMissing,
RestoreSeedMissing,
PrivateDataRedactionMissing,
SyncPlannerFeedMissing,
ServerConnectivityCheckRequested,
WalletExistsCheckRequested,
WalletLifecycleExecutionRequested,
SyncRequested,
SyncStatusPollingRequested,
WorkerQueueRequested,
WalletStateMutationRequested,
WalletFilePersistenceRequested,
SendImportExportRequested,
SettingsWriteRequested
};
struct LiteWalletLifecycleUiRequestIntent {
bool requestProvided = false;
LiteWalletLifecycleOperation operation = LiteWalletLifecycleOperation::CreateNew;
LiteWalletCreateRequest createRequest;
LiteWalletOpenRequest openRequest;
LiteWalletRestoreRequest restoreRequest;
};
struct LiteWalletLifecycleUiExecutionInput {
WalletCapabilities capabilities;
LiteWalletLifecycleUiRequestIntent request;
bool settingsLoaded = true;
bool requirePersistedServerSelectionIntent = true;
LiteWalletLifecycleUiPrerequisites ui;
bool settingsWriteRequested = false;
bool serverConnectivityCheckRequested = false;
bool walletExistsCheckRequested = false;
bool walletLifecycleExecutionRequested = false;
bool syncStartRequested = false;
bool syncStatusPollingRequested = false;
bool workerQueueEnqueueRequested = false;
bool walletStateMutationRequested = false;
bool walletFilePersistenceRequested = false;
bool sendImportExportExecutionRequested = false;
};
struct LiteWalletLifecycleUiExecutionIssueInfo {
LiteWalletLifecycleUiExecutionIssue issue = LiteWalletLifecycleUiExecutionIssue::SettingsNotLoaded;
std::string message;
};
struct LiteWalletLifecycleUiExecutionResult {
bool ok = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool noBridgeCalls = true;
bool noServerHealthChecked = true;
bool noWalletExistsChecked = true;
bool noWalletCreated = true;
bool noWalletOpened = true;
bool noWalletRestored = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWalletPersistence = true;
bool noSettingsPersistence = true;
bool noSendImportExportExecution = true;
bool settingsWritten = false;
bool workerQueueEnqueued = false;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool selectedServerSettingsLoaded = false;
bool selectedServerIntentAccepted = false;
bool lifecycleReadinessAccepted = false;
bool requestAccepted = false;
bool privateDataRedacted = false;
bool lifecycleReportCouldFeedSyncPlanners = false;
LiteWalletLifecycleUiExecutionStatus status = LiteWalletLifecycleUiExecutionStatus::WaitingForSettings;
std::string error;
std::string selectedServerUrlRedacted;
std::string requestSummaryRedacted;
std::string diagnosticSummary;
LiteConnectionSettings connectionSettings;
LiteServerSelectionResult selectedServer;
LiteWalletServerSelectionUiExecutionResult serverSelectionReport;
LiteWalletServerLifecycleReadinessInput lifecycleInput;
LiteWalletServerLifecycleReadinessResult lifecycleReadiness;
std::vector<LiteRedactedPrivateData> privateData;
std::vector<LiteWalletLifecycleUiExecutionIssueInfo> issues;
};
const char* liteWalletLifecycleUiExecutionStatusName(
LiteWalletLifecycleUiExecutionStatus status);
const char* liteWalletLifecycleUiExecutionIssueName(
LiteWalletLifecycleUiExecutionIssue issue);
LiteWalletLifecycleUiExecutionResult executeLiteWalletLifecycleUiRequest(
config::Settings& settings,
const LiteWalletLifecycleUiExecutionInput& input);
class LiteWalletLifecycleUiExecutionAdapter {
public:
explicit LiteWalletLifecycleUiExecutionAdapter(config::Settings& settings);
LiteWalletLifecycleUiExecutionResult execute(
const LiteWalletLifecycleUiExecutionInput& input) const;
private:
config::Settings& settings_;
};
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,205 @@
#include "wallet/lite_wallet_refresh_readiness_policy.h"
#include <utility>
namespace dragonx::wallet {
namespace {
void addIssue(LiteWalletRefreshReadinessResult& result,
LiteWalletRefreshReadinessIssue issue,
std::string message)
{
result.issues.push_back(LiteWalletRefreshReadinessIssueInfo{issue, std::move(message)});
}
std::string statusMessageOrDefault(const WalletBackendStatus& status,
const std::string& fallback)
{
return status.message.empty() ? fallback : status.message;
}
LiteWalletRefreshReadinessResult failReadiness(
LiteWalletRefreshReadinessResult result,
LiteWalletRefreshReadinessStatus status,
LiteWalletRefreshReadinessIssue issue,
std::string message)
{
result.status = status;
addIssue(result, issue, std::move(message));
result.error = result.issues.back().message;
return result;
}
bool coordinatorReportIsUnsafe(const LiteWalletAppRefreshCoordinationResult& coordination)
{
return !coordination.dryRunOnly ||
!coordination.noNetwork ||
coordination.stateMutationRequested ||
coordination.stateMutationAllowed ||
coordination.stateMutated ||
coordination.walletStateWritten;
}
} // namespace
const char* liteWalletRefreshReadinessStatusName(LiteWalletRefreshReadinessStatus status)
{
switch (status) {
case LiteWalletRefreshReadinessStatus::EligibleDryRun: return "EligibleDryRun";
case LiteWalletRefreshReadinessStatus::CoordinatorRejected: return "CoordinatorRejected";
case LiteWalletRefreshReadinessStatus::UnsafeReport: return "UnsafeReport";
case LiteWalletRefreshReadinessStatus::WaitingForLifecycle: return "WaitingForLifecycle";
case LiteWalletRefreshReadinessStatus::WaitingForSync: return "WaitingForSync";
case LiteWalletRefreshReadinessStatus::Stale: return "Stale";
}
return "Unknown";
}
const char* liteWalletRefreshReadinessIssueName(LiteWalletRefreshReadinessIssue issue)
{
switch (issue) {
case LiteWalletRefreshReadinessIssue::CoordinatorReportRejected: return "CoordinatorReportRejected";
case LiteWalletRefreshReadinessIssue::CoordinatorReportIncomplete: return "CoordinatorReportIncomplete";
case LiteWalletRefreshReadinessIssue::CoordinatorReportUnsafe: return "CoordinatorReportUnsafe";
case LiteWalletRefreshReadinessIssue::FullNodeRouteRejected: return "FullNodeRouteRejected";
case LiteWalletRefreshReadinessIssue::LifecycleNotReady: return "LifecycleNotReady";
case LiteWalletRefreshReadinessIssue::SyncNotReady: return "SyncNotReady";
case LiteWalletRefreshReadinessIssue::RefreshTimestampMissing: return "RefreshTimestampMissing";
case LiteWalletRefreshReadinessIssue::RefreshTimestampInFuture: return "RefreshTimestampInFuture";
case LiteWalletRefreshReadinessIssue::RefreshStale: return "RefreshStale";
case LiteWalletRefreshReadinessIssue::StaleRefreshAllowed: return "StaleRefreshAllowed";
}
return "Unknown";
}
LiteWalletRefreshReadinessResult evaluateLiteWalletRefreshReadiness(
const LiteWalletAppRefreshCoordinationResult& coordination,
const LiteWalletRefreshReadinessInputs& inputs,
LiteWalletRefreshReadinessPolicyOptions options)
{
LiteWalletRefreshReadinessResult result;
result.route = coordination.route;
result.dryRunOnly = coordination.dryRunOnly;
result.noNetwork = coordination.noNetwork;
result.stateMutationRequested = coordination.stateMutationRequested;
result.stateMutationAllowed = coordination.stateMutationAllowed;
result.stateMutated = coordination.stateMutated;
result.walletStateWritten = coordination.walletStateWritten;
result.lifecycleReady = inputs.lifecycleReady;
result.lifecycleStatus = inputs.lifecycleStatus;
result.syncReady = inputs.syncReady;
result.syncStatus = inputs.syncStatus;
result.maxRefreshAgeSeconds = options.maxRefreshAgeSeconds;
if (coordination.fullNodeRefreshDelegated || coordination.route == LiteWalletRefreshRouteKind::FullNodeRpc) {
return failReadiness(
std::move(result),
LiteWalletRefreshReadinessStatus::CoordinatorRejected,
LiteWalletRefreshReadinessIssue::FullNodeRouteRejected,
"full-node refresh reports are not eligible for lite UI refresh readiness");
}
if (!coordination.ok) {
return failReadiness(
std::move(result),
LiteWalletRefreshReadinessStatus::CoordinatorRejected,
LiteWalletRefreshReadinessIssue::CoordinatorReportRejected,
coordination.error.empty()
? "lite app refresh coordinator report is not successful"
: coordination.error);
}
result.coordinatorReportAccepted = true;
if (!coordination.mapped || !coordination.planned || !coordination.executionReported ||
coordination.successfulCommandCount == 0) {
return failReadiness(
std::move(result),
LiteWalletRefreshReadinessStatus::CoordinatorRejected,
LiteWalletRefreshReadinessIssue::CoordinatorReportIncomplete,
"lite app refresh coordinator report is incomplete");
}
if (coordinatorReportIsUnsafe(coordination)) {
return failReadiness(
std::move(result),
LiteWalletRefreshReadinessStatus::UnsafeReport,
LiteWalletRefreshReadinessIssue::CoordinatorReportUnsafe,
"lite app refresh coordinator report is not a no-network dry-run report");
}
if (options.requireLifecycleReady && !inputs.lifecycleReady) {
return failReadiness(
std::move(result),
LiteWalletRefreshReadinessStatus::WaitingForLifecycle,
LiteWalletRefreshReadinessIssue::LifecycleNotReady,
statusMessageOrDefault(inputs.lifecycleStatus, "lite wallet lifecycle is not ready"));
}
if (options.requireSyncReady && !inputs.syncReady) {
return failReadiness(
std::move(result),
LiteWalletRefreshReadinessStatus::WaitingForSync,
LiteWalletRefreshReadinessIssue::SyncNotReady,
statusMessageOrDefault(inputs.syncStatus, "lite wallet sync is not ready"));
}
if (options.requireFreshRefresh) {
if (!inputs.freshness.haveSnapshotTime) {
return failReadiness(
std::move(result),
LiteWalletRefreshReadinessStatus::Stale,
LiteWalletRefreshReadinessIssue::RefreshTimestampMissing,
"lite refresh snapshot time is not known");
}
if (inputs.freshness.snapshotTimeSeconds > inputs.freshness.nowSeconds) {
return failReadiness(
std::move(result),
LiteWalletRefreshReadinessStatus::Stale,
LiteWalletRefreshReadinessIssue::RefreshTimestampInFuture,
"lite refresh snapshot time is in the future");
}
result.refreshAgeSeconds = inputs.freshness.nowSeconds - inputs.freshness.snapshotTimeSeconds;
if (result.refreshAgeSeconds > options.maxRefreshAgeSeconds) {
result.refreshStale = true;
if (!options.allowStaleRefresh) {
return failReadiness(
std::move(result),
LiteWalletRefreshReadinessStatus::Stale,
LiteWalletRefreshReadinessIssue::RefreshStale,
"lite refresh coordinator report is stale");
}
result.staleRefreshAllowed = true;
addIssue(result,
LiteWalletRefreshReadinessIssue::StaleRefreshAllowed,
"stale lite refresh coordinator report is allowed for reporting only");
} else {
result.refreshFresh = true;
}
} else {
result.refreshFresh = true;
}
result.ok = true;
result.eligibleForFutureUiRefresh = true;
result.status = LiteWalletRefreshReadinessStatus::EligibleDryRun;
return result;
}
LiteWalletRefreshReadinessPolicy::LiteWalletRefreshReadinessPolicy(
LiteWalletRefreshReadinessPolicyOptions options)
: options_(options)
{
}
LiteWalletRefreshReadinessResult LiteWalletRefreshReadinessPolicy::evaluate(
const LiteWalletAppRefreshCoordinationResult& coordination,
const LiteWalletRefreshReadinessInputs& inputs) const
{
return evaluateLiteWalletRefreshReadiness(coordination, inputs, options_);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,106 @@
#pragma once
#include "lite_wallet_app_refresh_coordinator.h"
#include <cstdint>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletRefreshReadinessStatus {
EligibleDryRun,
CoordinatorRejected,
UnsafeReport,
WaitingForLifecycle,
WaitingForSync,
Stale,
};
enum class LiteWalletRefreshReadinessIssue {
CoordinatorReportRejected,
CoordinatorReportIncomplete,
CoordinatorReportUnsafe,
FullNodeRouteRejected,
LifecycleNotReady,
SyncNotReady,
RefreshTimestampMissing,
RefreshTimestampInFuture,
RefreshStale,
StaleRefreshAllowed,
};
struct LiteWalletRefreshFreshnessInput {
bool haveSnapshotTime = false;
std::uint64_t snapshotTimeSeconds = 0;
std::uint64_t nowSeconds = 0;
};
struct LiteWalletRefreshReadinessInputs {
bool lifecycleReady = false;
WalletBackendStatus lifecycleStatus;
bool syncReady = false;
WalletBackendStatus syncStatus;
LiteWalletRefreshFreshnessInput freshness;
};
struct LiteWalletRefreshReadinessPolicyOptions {
bool requireLifecycleReady = true;
bool requireSyncReady = true;
bool requireFreshRefresh = true;
bool allowStaleRefresh = false;
std::uint64_t maxRefreshAgeSeconds = 120;
};
struct LiteWalletRefreshReadinessIssueInfo {
LiteWalletRefreshReadinessIssue issue = LiteWalletRefreshReadinessIssue::CoordinatorReportRejected;
std::string message;
};
struct LiteWalletRefreshReadinessResult {
bool ok = false;
bool eligibleForFutureUiRefresh = false;
bool coordinatorReportAccepted = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool stateMutationRequested = false;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool lifecycleReady = false;
bool syncReady = false;
bool refreshFresh = false;
bool refreshStale = false;
bool staleRefreshAllowed = false;
LiteWalletRefreshReadinessStatus status = LiteWalletRefreshReadinessStatus::CoordinatorRejected;
LiteWalletRefreshRouteKind route = LiteWalletRefreshRouteKind::Unavailable;
WalletBackendStatus lifecycleStatus;
WalletBackendStatus syncStatus;
std::uint64_t refreshAgeSeconds = 0;
std::uint64_t maxRefreshAgeSeconds = 0;
std::vector<LiteWalletRefreshReadinessIssueInfo> issues;
std::string error;
};
const char* liteWalletRefreshReadinessStatusName(LiteWalletRefreshReadinessStatus status);
const char* liteWalletRefreshReadinessIssueName(LiteWalletRefreshReadinessIssue issue);
LiteWalletRefreshReadinessResult evaluateLiteWalletRefreshReadiness(
const LiteWalletAppRefreshCoordinationResult& coordination,
const LiteWalletRefreshReadinessInputs& inputs,
LiteWalletRefreshReadinessPolicyOptions options = {});
class LiteWalletRefreshReadinessPolicy {
public:
explicit LiteWalletRefreshReadinessPolicy(LiteWalletRefreshReadinessPolicyOptions options = {});
LiteWalletRefreshReadinessResult evaluate(
const LiteWalletAppRefreshCoordinationResult& coordination,
const LiteWalletRefreshReadinessInputs& inputs) const;
private:
LiteWalletRefreshReadinessPolicyOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,189 @@
#include "wallet/lite_wallet_refresh_service.h"
#include <utility>
namespace dragonx::wallet {
namespace {
WalletBackendStatus makeRefreshStatus(WalletBackendState state, std::string message)
{
WalletBackendStatus status;
status.state = state;
status.message = std::move(message);
return status;
}
} // namespace
LiteWalletGatewayRefreshAdapter::LiteWalletGatewayRefreshAdapter(LiteWalletGateway& gateway)
: gateway_(gateway)
{
}
LiteWalletRefreshPlan LiteWalletGatewayRefreshAdapter::planRefresh(
const LiteWalletRefreshRequest& request) const
{
return gateway_.planRefresh(request);
}
LiteWalletRefreshResult LiteWalletGatewayRefreshAdapter::refresh(
const LiteWalletRefreshRequest& request)
{
return gateway_.refresh(request);
}
WalletBackendStatus LiteWalletGatewayRefreshAdapter::status() const
{
return gateway_.status();
}
const char* liteWalletRefreshRouteKindName(LiteWalletRefreshRouteKind route)
{
switch (route) {
case LiteWalletRefreshRouteKind::FullNodeRpc: return "FullNodeRpc";
case LiteWalletRefreshRouteKind::LiteGateway: return "LiteGateway";
case LiteWalletRefreshRouteKind::Unavailable: return "Unavailable";
}
return "Unknown";
}
LiteWalletRefreshRouteKind liteWalletRefreshRouteForCapabilities(const WalletCapabilities& capabilities)
{
if (capabilities.fullNodeRpcAvailable) return LiteWalletRefreshRouteKind::FullNodeRpc;
if (isLiteBuild(capabilities) && supportsLiteBackend(capabilities)) {
return LiteWalletRefreshRouteKind::LiteGateway;
}
return LiteWalletRefreshRouteKind::Unavailable;
}
LiteWalletRefreshService::LiteWalletRefreshService(WalletCapabilities capabilities,
LiteWalletRefreshGateway* liteGateway,
LiteWalletRefreshServiceOptions options)
: capabilities_(capabilities), liteGateway_(liteGateway), options_(options)
{
}
LiteWalletRefreshRouteKind LiteWalletRefreshService::route() const
{
return liteWalletRefreshRouteForCapabilities(capabilities_);
}
WalletBackendStatus LiteWalletRefreshService::status() const
{
const auto selectedRoute = route();
if (selectedRoute == LiteWalletRefreshRouteKind::LiteGateway) {
if (!liteGateway_) return statusForRoute(selectedRoute, "lite wallet gateway is not configured");
return liteGateway_->status();
}
return statusForRoute(selectedRoute);
}
LiteWalletRefreshServicePlan LiteWalletRefreshService::planRefresh(
const LiteWalletRefreshRequest& request) const
{
LiteWalletRefreshServicePlan plan;
plan.route = route();
if (plan.route == LiteWalletRefreshRouteKind::FullNodeRpc) {
plan.ok = true;
plan.fullNodeRefreshDelegated = true;
plan.status = statusForRoute(plan.route);
return plan;
}
if (plan.route == LiteWalletRefreshRouteKind::Unavailable) {
plan.status = statusForRoute(plan.route);
plan.error = plan.status.message;
return plan;
}
if (!liteGateway_) {
plan.status = statusForRoute(plan.route, "lite wallet gateway is not configured");
plan.error = plan.status.message;
return plan;
}
plan.liteGatewayConfigured = true;
plan.liteGatewayExecutionAllowed = options_.allowLiteGatewayRefresh;
plan.liteGatewayPlan = liteGateway_->planRefresh(request);
if (!plan.liteGatewayPlan.ok) {
plan.status = makeRefreshStatus(WalletBackendState::Error, plan.liteGatewayPlan.error);
plan.error = plan.status.message;
return plan;
}
plan.ok = true;
plan.status = liteGateway_->status();
return plan;
}
LiteWalletRefreshServiceResult LiteWalletRefreshService::refresh(
const LiteWalletRefreshRequest& request)
{
auto plan = planRefresh(request);
if (!plan.ok) return resultFromPlan(plan);
if (plan.route == LiteWalletRefreshRouteKind::FullNodeRpc) {
auto result = resultFromPlan(plan);
result.ok = true;
return result;
}
if (!plan.liteGatewayExecutionAllowed) {
plan.status = makeRefreshStatus(
WalletBackendState::Unavailable,
"lite wallet refresh gateway execution is disabled");
plan.error = plan.status.message;
return resultFromPlan(plan);
}
LiteWalletRefreshServiceResult result;
result.plan = plan;
result.liteGatewayCalled = true;
result.liteGatewayResult = liteGateway_->refresh(request);
result.attempted = result.liteGatewayResult.attempted;
result.bundle = result.liteGatewayResult.bundle;
result.status = result.liteGatewayResult.status;
result.error = result.liteGatewayResult.error;
result.ok = result.liteGatewayResult.ok;
return result;
}
WalletBackendStatus LiteWalletRefreshService::statusForRoute(
LiteWalletRefreshRouteKind selectedRoute,
const std::string& detail) const
{
switch (selectedRoute) {
case LiteWalletRefreshRouteKind::FullNodeRpc:
return makeRefreshStatus(
WalletBackendState::Disconnected,
detail.empty()
? "full-node refresh remains handled by existing NetworkRefreshService"
: detail);
case LiteWalletRefreshRouteKind::LiteGateway:
return makeRefreshStatus(
detail.empty() ? WalletBackendState::Disconnected : WalletBackendState::Unavailable,
detail.empty()
? "lite wallet refresh route is ready for gateway planning"
: detail);
case LiteWalletRefreshRouteKind::Unavailable:
return makeRefreshStatus(
WalletBackendState::Unavailable,
detail.empty() ? "lite backend is not linked" : detail);
}
return makeRefreshStatus(WalletBackendState::Unavailable, "unknown wallet refresh route");
}
LiteWalletRefreshServiceResult LiteWalletRefreshService::resultFromPlan(
const LiteWalletRefreshServicePlan& plan) const
{
LiteWalletRefreshServiceResult result;
result.plan = plan;
result.fullNodeRefreshDelegated = plan.fullNodeRefreshDelegated;
result.status = plan.status;
result.error = plan.error;
return result;
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,92 @@
#pragma once
#include "lite_wallet_gateway.h"
#include "wallet_backend.h"
#include "wallet_capabilities.h"
#include <string>
namespace dragonx::wallet {
enum class LiteWalletRefreshRouteKind {
FullNodeRpc,
LiteGateway,
Unavailable,
};
struct LiteWalletRefreshServiceOptions {
bool allowLiteGatewayRefresh = false;
};
struct LiteWalletRefreshServicePlan {
bool ok = false;
LiteWalletRefreshRouteKind route = LiteWalletRefreshRouteKind::Unavailable;
bool fullNodeRefreshDelegated = false;
bool liteGatewayConfigured = false;
bool liteGatewayExecutionAllowed = false;
LiteWalletRefreshPlan liteGatewayPlan;
WalletBackendStatus status;
std::string error;
};
struct LiteWalletRefreshServiceResult {
bool ok = false;
bool attempted = false;
bool fullNodeRefreshDelegated = false;
bool liteGatewayCalled = false;
LiteWalletRefreshServicePlan plan;
LiteWalletRefreshResult liteGatewayResult;
LiteWalletRefreshBundle bundle;
WalletBackendStatus status;
std::string error;
};
class LiteWalletRefreshGateway {
public:
virtual ~LiteWalletRefreshGateway() = default;
virtual LiteWalletRefreshPlan planRefresh(const LiteWalletRefreshRequest& request) const = 0;
virtual LiteWalletRefreshResult refresh(const LiteWalletRefreshRequest& request) = 0;
virtual WalletBackendStatus status() const = 0;
};
class LiteWalletGatewayRefreshAdapter final : public LiteWalletRefreshGateway {
public:
explicit LiteWalletGatewayRefreshAdapter(LiteWalletGateway& gateway);
LiteWalletRefreshPlan planRefresh(const LiteWalletRefreshRequest& request) const override;
LiteWalletRefreshResult refresh(const LiteWalletRefreshRequest& request) override;
WalletBackendStatus status() const override;
private:
LiteWalletGateway& gateway_;
};
const char* liteWalletRefreshRouteKindName(LiteWalletRefreshRouteKind route);
LiteWalletRefreshRouteKind liteWalletRefreshRouteForCapabilities(const WalletCapabilities& capabilities);
class LiteWalletRefreshService {
public:
LiteWalletRefreshService(WalletCapabilities capabilities,
LiteWalletRefreshGateway* liteGateway = nullptr,
LiteWalletRefreshServiceOptions options = {});
const WalletCapabilities& capabilities() const { return capabilities_; }
const LiteWalletRefreshServiceOptions& options() const { return options_; }
LiteWalletRefreshRouteKind route() const;
WalletBackendStatus status() const;
LiteWalletRefreshServicePlan planRefresh(const LiteWalletRefreshRequest& request) const;
LiteWalletRefreshServiceResult refresh(const LiteWalletRefreshRequest& request);
private:
WalletBackendStatus statusForRoute(LiteWalletRefreshRouteKind route,
const std::string& detail = {}) const;
LiteWalletRefreshServiceResult resultFromPlan(const LiteWalletRefreshServicePlan& plan) const;
WalletCapabilities capabilities_;
LiteWalletRefreshGateway* liteGateway_ = nullptr;
LiteWalletRefreshServiceOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,407 @@
#include "wallet/lite_wallet_server_lifecycle_readiness.h"
#include <cctype>
#include <utility>
namespace dragonx::wallet {
namespace {
std::string trimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
void addIssue(LiteWalletServerLifecycleReadinessResult& result,
LiteWalletServerLifecycleReadinessIssue issue,
std::string message)
{
result.issues.push_back(LiteWalletServerLifecycleReadinessIssueInfo{issue, std::move(message)});
}
LiteWalletServerLifecycleReadinessResult stoppedResult(
LiteWalletServerLifecycleReadinessResult result,
LiteWalletServerLifecycleReadinessStatus status,
LiteWalletServerLifecycleReadinessIssue issue,
std::string message)
{
result.status = status;
addIssue(result, issue, std::move(message));
result.error = result.issues.back().message;
result.lifecycleStatus = WalletBackendStatus{WalletBackendState::Unavailable, result.error, {}, {}, 0.0};
result.syncLifecycleInput.status = result.lifecycleStatus;
return result;
}
LiteRedactedPrivateData redactedPrivateField(LitePrivateDataKind kind, const std::string& value)
{
return LiteRedactedPrivateData{kind, !trimCopy(value).empty(), redactLitePrivateDataValue(value)};
}
bool privateDataIsRedacted(const std::vector<LiteRedactedPrivateData>& privateData)
{
for (const auto& item : privateData) {
if (!item.present && item.redactedValue != "<empty>") return false;
if (item.present && item.redactedValue != "<redacted>") return false;
}
return true;
}
LiteWalletSelectedServerDisplayReport displayReportForSelection(
const LiteServerSelectionResult& selection)
{
LiteWalletSelectedServerDisplayReport report;
if (!selection.ok) {
report.status = LiteWalletSelectedServerDisplayStatus::Missing;
report.message = selection.error.empty() ? "no usable lite server is selected" : selection.error;
return report;
}
report.ok = true;
report.status = selection.customServer
? LiteWalletSelectedServerDisplayStatus::CustomServer
: LiteWalletSelectedServerDisplayStatus::SelectedServer;
report.label = selection.server.label;
report.url = selection.server.url;
report.serverIndex = selection.serverIndex;
report.customServer = selection.customServer;
report.message = selection.customServer
? "custom lite server selected for display"
: "configured lite server selected for display";
return report;
}
LiteWalletLifecyclePlan lifecyclePlanForRequest(
LiteWalletLifecycleOperation operation,
const LiteServerSelectionResult& selection)
{
LiteWalletLifecyclePlan plan;
plan.operation = operation;
plan.bridgeExecutionAllowed = false;
if (!selection.ok) {
plan.error = selection.error.empty() ? "no usable lite server is selected" : selection.error;
return plan;
}
plan.ok = true;
plan.server = selection.server;
plan.serverIndex = selection.serverIndex;
plan.customServer = selection.customServer;
return plan;
}
LiteWalletLifecycleUiRequestPlan requestPlanForInput(
const LiteWalletServerLifecycleReadinessInput& input,
const LiteServerSelectionResult& selection)
{
LiteWalletLifecycleUiRequestPlan plan;
plan.operation = input.operation;
plan.lifecyclePlan = lifecyclePlanForRequest(input.operation, selection);
if (!plan.lifecyclePlan.ok) {
plan.error = plan.lifecyclePlan.error;
return plan;
}
switch (input.operation) {
case LiteWalletLifecycleOperation::CreateNew:
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::Passphrase,
input.createRequest.passphrase));
plan.requestSummary = "operation=create;server=<selected>;passphrase=" +
std::string(input.createRequest.passphrase.empty() ? "<empty>" : "<redacted>");
break;
case LiteWalletLifecycleOperation::OpenExisting:
if (trimCopy(input.openRequest.walletPath).empty()) {
plan.error = "lite wallet open requires a wallet path selected by the UI";
return plan;
}
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::WalletPath,
input.openRequest.walletPath));
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::Passphrase,
input.openRequest.passphrase));
plan.requestSummary = "operation=open;server=<selected>;wallet_path=<redacted>;passphrase=" +
std::string(input.openRequest.passphrase.empty() ? "<empty>" : "<redacted>");
break;
case LiteWalletLifecycleOperation::RestoreFromSeed:
if (trimCopy(input.restoreRequest.seedPhrase).empty()) {
plan.error = "lite wallet restore requires redacted seed material metadata";
return plan;
}
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::SeedPhrase,
input.restoreRequest.seedPhrase));
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::WalletPath,
input.restoreRequest.walletPath));
plan.privateData.push_back(redactedPrivateField(
LitePrivateDataKind::Passphrase,
input.restoreRequest.passphrase));
plan.requestSummary = "operation=restore;server=<selected>;seed=<redacted>;wallet_path=" +
std::string(input.restoreRequest.walletPath.empty() ? "<empty>" : "<redacted>") +
";passphrase=" +
std::string(input.restoreRequest.passphrase.empty() ? "<empty>" : "<redacted>");
break;
}
plan.privateInputsRedacted = privateDataIsRedacted(plan.privateData);
plan.lifecyclePlan.privateData = plan.privateData;
plan.ok = plan.privateInputsRedacted;
if (!plan.ok) plan.error = "lite wallet lifecycle private inputs are not redacted";
return plan;
}
LiteWalletServerSelectionPersistencePlan persistencePlanForInput(
const LiteWalletServerLifecycleReadinessInput& input,
const LiteServerSelectionResult& selection)
{
LiteWalletServerSelectionPersistencePlan plan;
plan.settingsLoaded = input.persistence.settingsLoaded;
plan.persistedSelectionIntentAccepted = input.persistence.havePersistedSelectionIntent;
plan.wouldPersistSelectedServer = input.persistence.persistSelectedServer;
plan.persistenceOwnerAccepted = input.persistence.persistenceOwnerReady;
plan.selectionMode = input.settings.selectionMode;
if (selection.ok) {
plan.selectedServerUrl = selection.server.url;
plan.selectedServerIndex = selection.serverIndex;
plan.selectedServerCustom = selection.customServer;
}
plan.settingsWritten = false;
plan.ok = true;
return plan;
}
WalletBackendStatus readyLifecycleStatus(const LiteWalletSelectedServerDisplayReport& display)
{
const std::string serverLabel = display.label.empty() ? display.url : display.label;
return WalletBackendStatus{
WalletBackendState::Disconnected,
"lite lifecycle UI readiness accepted for " + serverLabel + "; wallet lifecycle execution is still disabled",
{},
{},
0.0
};
}
} // namespace
const char* liteWalletServerLifecycleReadinessStatusName(
LiteWalletServerLifecycleReadinessStatus status)
{
switch (status) {
case LiteWalletServerLifecycleReadinessStatus::ReadyForFutureLifecycle: return "ReadyForFutureLifecycle";
case LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild: return "WaitingForLiteBuild";
case LiteWalletServerLifecycleReadinessStatus::WaitingForBackendCapability: return "WaitingForBackendCapability";
case LiteWalletServerLifecycleReadinessStatus::WaitingForServerSelection: return "WaitingForServerSelection";
case LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent: return "WaitingForPersistenceIntent";
case LiteWalletServerLifecycleReadinessStatus::WaitingForDisplayStatus: return "WaitingForDisplayStatus";
case LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi: return "WaitingForLifecycleUi";
case LiteWalletServerLifecycleReadinessStatus::WaitingForPrivateDataRedaction: return "WaitingForPrivateDataRedaction";
case LiteWalletServerLifecycleReadinessStatus::WaitingForSyncPlannerFeed: return "WaitingForSyncPlannerFeed";
case LiteWalletServerLifecycleReadinessStatus::RuntimeExecutionDisabled: return "RuntimeExecutionDisabled";
}
return "Unknown";
}
const char* liteWalletServerLifecycleReadinessIssueName(
LiteWalletServerLifecycleReadinessIssue issue)
{
switch (issue) {
case LiteWalletServerLifecycleReadinessIssue::FullNodeBuild: return "FullNodeBuild";
case LiteWalletServerLifecycleReadinessIssue::LiteBackendCapabilityMissing: return "LiteBackendCapabilityMissing";
case LiteWalletServerLifecycleReadinessIssue::PersistedSettingsNotLoaded: return "PersistedSettingsNotLoaded";
case LiteWalletServerLifecycleReadinessIssue::PersistedServerSelectionIntentMissing: return "PersistedServerSelectionIntentMissing";
case LiteWalletServerLifecycleReadinessIssue::ServerSelectionMissing: return "ServerSelectionMissing";
case LiteWalletServerLifecycleReadinessIssue::ServerPersistenceOwnerMissing: return "ServerPersistenceOwnerMissing";
case LiteWalletServerLifecycleReadinessIssue::SelectedServerDisplayMissing: return "SelectedServerDisplayMissing";
case LiteWalletServerLifecycleReadinessIssue::LifecycleUiOwnerMissing: return "LifecycleUiOwnerMissing";
case LiteWalletServerLifecycleReadinessIssue::LifecycleOperationUnconfirmed: return "LifecycleOperationUnconfirmed";
case LiteWalletServerLifecycleReadinessIssue::OpenWalletPathMissing: return "OpenWalletPathMissing";
case LiteWalletServerLifecycleReadinessIssue::RestoreSeedMissing: return "RestoreSeedMissing";
case LiteWalletServerLifecycleReadinessIssue::PrivateDataRedactionMissing: return "PrivateDataRedactionMissing";
case LiteWalletServerLifecycleReadinessIssue::SyncPlannerFeedMissing: return "SyncPlannerFeedMissing";
case LiteWalletServerLifecycleReadinessIssue::RealLifecycleExecutionRequested: return "RealLifecycleExecutionRequested";
}
return "Unknown";
}
const char* liteWalletSelectedServerDisplayStatusName(
LiteWalletSelectedServerDisplayStatus status)
{
switch (status) {
case LiteWalletSelectedServerDisplayStatus::Hidden: return "Hidden";
case LiteWalletSelectedServerDisplayStatus::SelectedServer: return "SelectedServer";
case LiteWalletSelectedServerDisplayStatus::CustomServer: return "CustomServer";
case LiteWalletSelectedServerDisplayStatus::Missing: return "Missing";
}
return "Unknown";
}
LiteWalletServerLifecycleReadinessResult evaluateLiteWalletServerLifecycleReadiness(
const LiteWalletServerLifecycleReadinessInput& input,
LiteWalletServerLifecycleReadinessOptions options)
{
LiteWalletServerLifecycleReadinessResult result;
result.capabilities = input.capabilities;
result.persistencePlan = persistencePlanForInput(input, {});
if (options.requireLiteBuild && !isLiteBuild(input.capabilities)) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild,
LiteWalletServerLifecycleReadinessIssue::FullNodeBuild,
"lite server lifecycle readiness requires a lite build");
}
result.liteBuildAccepted = true;
if (options.requireLiteBackendCapability && !supportsLiteBackend(input.capabilities)) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForBackendCapability,
LiteWalletServerLifecycleReadinessIssue::LiteBackendCapabilityMissing,
"lite backend capability is required before lifecycle UI readiness can feed sync planners");
}
result.backendCapabilityAccepted = true;
if (options.requirePersistedSettingsLoaded && !input.persistence.settingsLoaded) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent,
LiteWalletServerLifecycleReadinessIssue::PersistedSettingsNotLoaded,
"lite server settings must be loaded before lifecycle UI readiness is evaluated");
}
if (options.requirePersistedSelectionIntent && !input.persistence.havePersistedSelectionIntent) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent,
LiteWalletServerLifecycleReadinessIssue::PersistedServerSelectionIntentMissing,
"lite server selection persistence intent is missing");
}
result.selectedServer = selectLiteServer(input.settings);
result.persistencePlan = persistencePlanForInput(input, result.selectedServer);
if (!result.selectedServer.ok) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForServerSelection,
LiteWalletServerLifecycleReadinessIssue::ServerSelectionMissing,
result.selectedServer.error.empty()
? "no usable lite server is selected"
: result.selectedServer.error);
}
result.serverSelectionAccepted = true;
if (input.persistence.persistSelectedServer && !input.persistence.persistenceOwnerReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent,
LiteWalletServerLifecycleReadinessIssue::ServerPersistenceOwnerMissing,
"lite server selection persistence owner is not ready");
}
result.persistenceIntentAccepted = true;
result.persistencePlan.ok = true;
result.selectedServerDisplay = displayReportForSelection(result.selectedServer);
if (options.requireSelectedServerDisplay && !input.ui.selectedServerDisplayReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForDisplayStatus,
LiteWalletServerLifecycleReadinessIssue::SelectedServerDisplayMissing,
"selected lite server display status is not ready");
}
result.selectedServerDisplayAccepted = true;
if (options.requireLifecycleUiOwner && !input.ui.lifecycleUiOwnerReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi,
LiteWalletServerLifecycleReadinessIssue::LifecycleUiOwnerMissing,
"lite wallet lifecycle UI owner is not ready");
}
result.lifecycleUiOwnerAccepted = true;
if (input.ui.realLifecycleExecutionRequested) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::RuntimeExecutionDisabled,
LiteWalletServerLifecycleReadinessIssue::RealLifecycleExecutionRequested,
"real lite wallet lifecycle execution is disabled in this scaffold");
}
if (options.requireOperationConfirmation && !input.ui.operationConfirmed) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi,
LiteWalletServerLifecycleReadinessIssue::LifecycleOperationUnconfirmed,
"lite wallet lifecycle operation requires explicit UI confirmation");
}
result.requestPlan = requestPlanForInput(input, result.selectedServer);
if (!result.requestPlan.ok) {
const auto issue = input.operation == LiteWalletLifecycleOperation::OpenExisting
? LiteWalletServerLifecycleReadinessIssue::OpenWalletPathMissing
: LiteWalletServerLifecycleReadinessIssue::RestoreSeedMissing;
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi,
issue,
result.requestPlan.error.empty()
? "lite wallet lifecycle UI request is incomplete"
: result.requestPlan.error);
}
result.requestAccepted = true;
if (options.requirePrivateDataRedaction && !input.ui.privateDataRedactionReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForPrivateDataRedaction,
LiteWalletServerLifecycleReadinessIssue::PrivateDataRedactionMissing,
"lite wallet lifecycle private-data redaction owner is not ready");
}
result.privateDataRedactionAccepted = true;
if (options.requireSyncPlannerFeed && !input.ui.syncPlannerFeedReady) {
return stoppedResult(
std::move(result),
LiteWalletServerLifecycleReadinessStatus::WaitingForSyncPlannerFeed,
LiteWalletServerLifecycleReadinessIssue::SyncPlannerFeedMissing,
"lite lifecycle readiness feed for sync planners is not ready");
}
result.syncPlannerFeedAccepted = true;
result.ok = true;
result.status = LiteWalletServerLifecycleReadinessStatus::ReadyForFutureLifecycle;
result.futureLifecycleCouldBeEnabled = true;
result.lifecycleReportCouldFeedSyncPlanners = true;
result.lifecycleStatus = readyLifecycleStatus(result.selectedServerDisplay);
result.syncLifecycleInput.ready = true;
result.syncLifecycleInput.status = result.lifecycleStatus;
return result;
}
LiteWalletSyncAppRefreshLifecycleInput liteWalletSyncLifecycleInputFromServerLifecycleReadiness(
const LiteWalletServerLifecycleReadinessResult& result)
{
return result.syncLifecycleInput;
}
LiteWalletServerLifecycleReadinessPlanner::LiteWalletServerLifecycleReadinessPlanner(
LiteWalletServerLifecycleReadinessOptions options)
: options_(options)
{
}
LiteWalletServerLifecycleReadinessResult LiteWalletServerLifecycleReadinessPlanner::evaluate(
const LiteWalletServerLifecycleReadinessInput& input) const
{
return evaluateLiteWalletServerLifecycleReadiness(input, options_);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,197 @@
#pragma once
#include "lite_wallet_lifecycle_service.h"
#include "lite_wallet_sync_app_refresh_integration.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletServerLifecycleReadinessStatus {
ReadyForFutureLifecycle,
WaitingForLiteBuild,
WaitingForBackendCapability,
WaitingForServerSelection,
WaitingForPersistenceIntent,
WaitingForDisplayStatus,
WaitingForLifecycleUi,
WaitingForPrivateDataRedaction,
WaitingForSyncPlannerFeed,
RuntimeExecutionDisabled,
};
enum class LiteWalletServerLifecycleReadinessIssue {
FullNodeBuild,
LiteBackendCapabilityMissing,
PersistedSettingsNotLoaded,
PersistedServerSelectionIntentMissing,
ServerSelectionMissing,
ServerPersistenceOwnerMissing,
SelectedServerDisplayMissing,
LifecycleUiOwnerMissing,
LifecycleOperationUnconfirmed,
OpenWalletPathMissing,
RestoreSeedMissing,
PrivateDataRedactionMissing,
SyncPlannerFeedMissing,
RealLifecycleExecutionRequested,
};
enum class LiteWalletSelectedServerDisplayStatus {
Hidden,
SelectedServer,
CustomServer,
Missing,
};
struct LiteWalletServerSelectionPersistenceInput {
bool settingsLoaded = false;
bool havePersistedSelectionIntent = false;
bool persistSelectedServer = false;
bool persistenceOwnerReady = false;
};
struct LiteWalletLifecycleUiPrerequisites {
bool selectedServerDisplayReady = false;
bool lifecycleUiOwnerReady = false;
bool operationConfirmed = false;
bool privateDataRedactionReady = false;
bool syncPlannerFeedReady = false;
bool realLifecycleExecutionRequested = false;
};
struct LiteWalletServerLifecycleReadinessInput {
WalletCapabilities capabilities;
LiteConnectionSettings settings;
LiteWalletLifecycleOperation operation = LiteWalletLifecycleOperation::CreateNew;
LiteWalletServerSelectionPersistenceInput persistence;
LiteWalletLifecycleUiPrerequisites ui;
LiteWalletCreateRequest createRequest;
LiteWalletOpenRequest openRequest;
LiteWalletRestoreRequest restoreRequest;
};
struct LiteWalletServerLifecycleReadinessOptions {
bool requireLiteBuild = true;
bool requireLiteBackendCapability = true;
bool requirePersistedSettingsLoaded = true;
bool requirePersistedSelectionIntent = true;
bool requireSelectedServerDisplay = true;
bool requireLifecycleUiOwner = true;
bool requireOperationConfirmation = true;
bool requirePrivateDataRedaction = true;
bool requireSyncPlannerFeed = true;
};
struct LiteWalletSelectedServerDisplayReport {
bool ok = false;
LiteWalletSelectedServerDisplayStatus status = LiteWalletSelectedServerDisplayStatus::Hidden;
std::string label;
std::string url;
std::size_t serverIndex = 0;
bool customServer = false;
std::string message;
};
struct LiteWalletServerSelectionPersistencePlan {
bool ok = false;
bool settingsLoaded = false;
bool persistedSelectionIntentAccepted = false;
bool wouldPersistSelectedServer = false;
bool persistenceOwnerAccepted = false;
bool settingsWritten = false;
LiteServerSelectionMode selectionMode = LiteServerSelectionMode::Sticky;
std::string selectedServerUrl;
std::size_t selectedServerIndex = 0;
bool selectedServerCustom = false;
std::string error;
};
struct LiteWalletLifecycleUiRequestPlan {
bool ok = false;
LiteWalletLifecycleOperation operation = LiteWalletLifecycleOperation::CreateNew;
LiteWalletLifecyclePlan lifecyclePlan;
std::vector<LiteRedactedPrivateData> privateData;
bool privateInputsRedacted = true;
std::string requestSummary;
std::string error;
};
struct LiteWalletServerLifecycleReadinessIssueInfo {
LiteWalletServerLifecycleReadinessIssue issue = LiteWalletServerLifecycleReadinessIssue::FullNodeBuild;
std::string message;
};
struct LiteWalletServerLifecycleReadinessResult {
bool ok = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool noBridgeCalls = true;
bool noServerHealthChecked = true;
bool noWalletExistsChecked = true;
bool noWalletCreated = true;
bool noWalletOpened = true;
bool noWalletRestored = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWalletPersistence = true;
bool noSettingsPersistence = true;
bool settingsWritten = false;
bool workerQueueEnqueued = false;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool walletReady = false;
bool liteBuildAccepted = false;
bool backendCapabilityAccepted = false;
bool serverSelectionAccepted = false;
bool persistenceIntentAccepted = false;
bool selectedServerDisplayAccepted = false;
bool lifecycleUiOwnerAccepted = false;
bool requestAccepted = false;
bool privateDataRedactionAccepted = false;
bool syncPlannerFeedAccepted = false;
bool futureLifecycleCouldBeEnabled = false;
bool lifecycleReportCouldFeedSyncPlanners = false;
LiteWalletServerLifecycleReadinessStatus status = LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild;
WalletCapabilities capabilities;
LiteServerSelectionResult selectedServer;
LiteWalletSelectedServerDisplayReport selectedServerDisplay;
LiteWalletServerSelectionPersistencePlan persistencePlan;
LiteWalletLifecycleUiRequestPlan requestPlan;
LiteWalletSyncAppRefreshLifecycleInput syncLifecycleInput;
WalletBackendStatus lifecycleStatus;
std::vector<LiteWalletServerLifecycleReadinessIssueInfo> issues;
std::string error;
};
const char* liteWalletServerLifecycleReadinessStatusName(
LiteWalletServerLifecycleReadinessStatus status);
const char* liteWalletServerLifecycleReadinessIssueName(
LiteWalletServerLifecycleReadinessIssue issue);
const char* liteWalletSelectedServerDisplayStatusName(
LiteWalletSelectedServerDisplayStatus status);
LiteWalletServerLifecycleReadinessResult evaluateLiteWalletServerLifecycleReadiness(
const LiteWalletServerLifecycleReadinessInput& input,
LiteWalletServerLifecycleReadinessOptions options = {});
LiteWalletSyncAppRefreshLifecycleInput liteWalletSyncLifecycleInputFromServerLifecycleReadiness(
const LiteWalletServerLifecycleReadinessResult& result);
class LiteWalletServerLifecycleReadinessPlanner {
public:
explicit LiteWalletServerLifecycleReadinessPlanner(
LiteWalletServerLifecycleReadinessOptions options = {});
LiteWalletServerLifecycleReadinessResult evaluate(
const LiteWalletServerLifecycleReadinessInput& input) const;
private:
LiteWalletServerLifecycleReadinessOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,463 @@
#include "lite_wallet_server_selection_adapter.h"
#include <algorithm>
#include <cctype>
#include <sstream>
#include <utility>
namespace dragonx {
namespace wallet {
namespace {
std::string trimCopy(const std::string& value)
{
const auto first = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
return std::isspace(ch) != 0;
});
const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
return std::isspace(ch) != 0;
}).base();
if (first >= last) return {};
return std::string(first, last);
}
LiteServerSelectionMode walletModeFromSettings(
config::Settings::LiteServerSelectionPreferenceMode mode)
{
switch (mode) {
case config::Settings::LiteServerSelectionPreferenceMode::Sticky:
return LiteServerSelectionMode::Sticky;
case config::Settings::LiteServerSelectionPreferenceMode::Random:
return LiteServerSelectionMode::Random;
}
return LiteServerSelectionMode::Sticky;
}
config::Settings::LiteServerSelectionPreferenceMode settingsModeFromWallet(
LiteServerSelectionMode mode)
{
switch (mode) {
case LiteServerSelectionMode::Sticky:
return config::Settings::LiteServerSelectionPreferenceMode::Sticky;
case LiteServerSelectionMode::Random:
return config::Settings::LiteServerSelectionPreferenceMode::Random;
}
return config::Settings::LiteServerSelectionPreferenceMode::Sticky;
}
void addIssue(std::vector<LiteWalletServerSelectionUiExecutionIssueInfo>& issues,
LiteWalletServerSelectionUiExecutionIssue issue,
std::string message)
{
issues.push_back(LiteWalletServerSelectionUiExecutionIssueInfo{issue, std::move(message)});
}
LiteWalletServerSelectionUiExecutionStatus statusFromLifecycle(
LiteWalletServerLifecycleReadinessStatus status)
{
switch (status) {
case LiteWalletServerLifecycleReadinessStatus::ReadyForFutureLifecycle:
return LiteWalletServerSelectionUiExecutionStatus::ReadyForFutureLifecycle;
case LiteWalletServerLifecycleReadinessStatus::WaitingForLiteBuild:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForLiteBuild;
case LiteWalletServerLifecycleReadinessStatus::WaitingForBackendCapability:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForBackendCapability;
case LiteWalletServerLifecycleReadinessStatus::WaitingForPersistenceIntent:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings;
case LiteWalletServerLifecycleReadinessStatus::WaitingForServerSelection:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection;
case LiteWalletServerLifecycleReadinessStatus::WaitingForDisplayStatus:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForDisplayStatus;
case LiteWalletServerLifecycleReadinessStatus::WaitingForLifecycleUi:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForLifecycleUi;
case LiteWalletServerLifecycleReadinessStatus::WaitingForPrivateDataRedaction:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForPrivateDataRedaction;
case LiteWalletServerLifecycleReadinessStatus::WaitingForSyncPlannerFeed:
return LiteWalletServerSelectionUiExecutionStatus::WaitingForSyncPlannerFeed;
case LiteWalletServerLifecycleReadinessStatus::RuntimeExecutionDisabled:
return LiteWalletServerSelectionUiExecutionStatus::RuntimeExecutionDisabled;
}
return LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings;
}
LiteWalletServerSelectionUiExecutionIssue issueFromLifecycle(
LiteWalletServerLifecycleReadinessIssue issue)
{
switch (issue) {
case LiteWalletServerLifecycleReadinessIssue::FullNodeBuild:
return LiteWalletServerSelectionUiExecutionIssue::FullNodeBuild;
case LiteWalletServerLifecycleReadinessIssue::LiteBackendCapabilityMissing:
return LiteWalletServerSelectionUiExecutionIssue::LiteBackendCapabilityMissing;
case LiteWalletServerLifecycleReadinessIssue::PersistedSettingsNotLoaded:
case LiteWalletServerLifecycleReadinessIssue::PersistedServerSelectionIntentMissing:
return LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded;
case LiteWalletServerLifecycleReadinessIssue::ServerSelectionMissing:
return LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing;
case LiteWalletServerLifecycleReadinessIssue::ServerPersistenceOwnerMissing:
return LiteWalletServerSelectionUiExecutionIssue::ServerPersistenceOwnerMissing;
case LiteWalletServerLifecycleReadinessIssue::SelectedServerDisplayMissing:
return LiteWalletServerSelectionUiExecutionIssue::SelectedServerDisplayMissing;
case LiteWalletServerLifecycleReadinessIssue::LifecycleUiOwnerMissing:
case LiteWalletServerLifecycleReadinessIssue::LifecycleOperationUnconfirmed:
case LiteWalletServerLifecycleReadinessIssue::OpenWalletPathMissing:
case LiteWalletServerLifecycleReadinessIssue::RestoreSeedMissing:
return LiteWalletServerSelectionUiExecutionIssue::LifecycleUiOwnerMissing;
case LiteWalletServerLifecycleReadinessIssue::PrivateDataRedactionMissing:
return LiteWalletServerSelectionUiExecutionIssue::PrivateDataRedactionMissing;
case LiteWalletServerLifecycleReadinessIssue::SyncPlannerFeedMissing:
return LiteWalletServerSelectionUiExecutionIssue::SyncPlannerFeedMissing;
case LiteWalletServerLifecycleReadinessIssue::RealLifecycleExecutionRequested:
return LiteWalletServerSelectionUiExecutionIssue::WalletLifecycleExecutionRequested;
}
return LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded;
}
void copyLifecycleIssues(LiteWalletServerSelectionUiExecutionResult& result)
{
for (const auto& issue : result.lifecycleReadiness.issues) {
addIssue(result.issues, issueFromLifecycle(issue.issue), issue.message);
}
}
LiteWalletServerSelectionUiExecutionResult stoppedResult(
LiteWalletServerSelectionUiExecutionResult result,
LiteWalletServerSelectionUiExecutionStatus status,
LiteWalletServerSelectionUiExecutionIssue issue,
const std::string& message)
{
result.status = status;
result.error = message;
result.ok = false;
addIssue(result.issues, issue, message);
return result;
}
std::string buildDiagnosticSummary(const LiteWalletServerSelectionUiExecutionResult& result)
{
std::ostringstream out;
out << "mode=" << liteServerSelectionModeName(result.connectionSettings.selectionMode)
<< ";server=" << result.selectedServerUrlRedacted
<< ";settings_written=" << (result.settingsWritten ? "true" : "false")
<< ";network=false;bridge=false;lifecycle=false;sync=false;wallet_state=false";
return out.str();
}
LiteConnectionSettings settingsWithIntent(const LiteConnectionSettings& current,
const LiteWalletServerSelectionUiIntent& intent)
{
LiteConnectionSettings settings = current;
if (!intent.selectedServerIntentProvided) return settings;
settings.selectionMode = intent.selectionMode;
if (!intent.chainName.empty()) settings.chainName = trimCopy(intent.chainName);
if (intent.replaceServers) settings.servers = intent.servers;
switch (intent.selectionMode) {
case LiteServerSelectionMode::Sticky:
settings.stickyServerUrl = trimCopy(intent.selectedServerUrl);
break;
case LiteServerSelectionMode::Random:
settings.randomSelectionSeed = intent.randomSelectionSeed;
break;
}
return settings;
}
LiteWalletServerLifecycleReadinessInput makeLifecycleInput(
const LiteWalletServerSelectionUiExecutionInput& input,
const LiteConnectionSettings& connectionSettings)
{
LiteWalletServerLifecycleReadinessInput lifecycle;
lifecycle.capabilities = input.capabilities;
lifecycle.settings = connectionSettings;
lifecycle.operation = input.operation;
lifecycle.persistence.settingsLoaded = input.persistence.settingsLoaded;
lifecycle.persistence.havePersistedSelectionIntent =
input.persistence.havePersistedSelectionIntent || input.intent.selectedServerIntentProvided;
lifecycle.persistence.persistSelectedServer = input.persistence.persistSelectedServer;
lifecycle.persistence.persistenceOwnerReady = input.persistence.persistenceOwnerReady;
lifecycle.ui = input.ui;
lifecycle.ui.realLifecycleExecutionRequested =
lifecycle.ui.realLifecycleExecutionRequested || input.walletLifecycleExecutionRequested;
lifecycle.createRequest = input.createRequest;
lifecycle.openRequest = input.openRequest;
lifecycle.restoreRequest = input.restoreRequest;
return lifecycle;
}
} // namespace
LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Settings& settings)
{
LiteConnectionSettings connectionSettings;
connectionSettings.servers.clear();
for (const auto& server : settings.getLiteServers()) {
connectionSettings.servers.push_back(LiteServerEndpoint{
trimCopy(server.url),
server.label,
server.enabled
});
}
connectionSettings.selectionMode = walletModeFromSettings(settings.getLiteServerSelectionMode());
connectionSettings.stickyServerUrl = trimCopy(settings.getLiteStickyServerUrl());
connectionSettings.chainName = trimCopy(settings.getLiteChainName());
if (connectionSettings.chainName.empty()) connectionSettings.chainName = kDragonXLiteChainName;
connectionSettings.randomSelectionSeed = settings.getLiteRandomSelectionSeed();
return connectionSettings;
}
void applyLiteConnectionSettingsToAppSettings(config::Settings& settings,
const LiteConnectionSettings& connectionSettings)
{
std::vector<config::Settings::LiteServerPreference> servers;
servers.reserve(connectionSettings.servers.size());
for (const auto& server : connectionSettings.servers) {
servers.push_back(config::Settings::LiteServerPreference{
trimCopy(server.url),
server.label,
server.enabled
});
}
settings.setLiteServerSelectionMode(settingsModeFromWallet(connectionSettings.selectionMode));
settings.setLiteStickyServerUrl(trimCopy(connectionSettings.stickyServerUrl));
settings.setLiteChainName(trimCopy(connectionSettings.chainName).empty()
? kDragonXLiteChainName
: trimCopy(connectionSettings.chainName));
settings.setLiteRandomSelectionSeed(connectionSettings.randomSelectionSeed);
settings.setLiteServers(servers);
}
const char* liteWalletServerSelectionUiExecutionStatusName(
LiteWalletServerSelectionUiExecutionStatus status)
{
switch (status) {
case LiteWalletServerSelectionUiExecutionStatus::ReadyForFutureLifecycle:
return "ReadyForFutureLifecycle";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForLiteBuild:
return "WaitingForLiteBuild";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForBackendCapability:
return "WaitingForBackendCapability";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings:
return "WaitingForSettings";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection:
return "WaitingForServerSelection";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForPersistenceOwner:
return "WaitingForPersistenceOwner";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForDisplayStatus:
return "WaitingForDisplayStatus";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForLifecycleUi:
return "WaitingForLifecycleUi";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForPrivateDataRedaction:
return "WaitingForPrivateDataRedaction";
case LiteWalletServerSelectionUiExecutionStatus::WaitingForSyncPlannerFeed:
return "WaitingForSyncPlannerFeed";
case LiteWalletServerSelectionUiExecutionStatus::SettingsSaveFailed:
return "SettingsSaveFailed";
case LiteWalletServerSelectionUiExecutionStatus::RuntimeExecutionDisabled:
return "RuntimeExecutionDisabled";
}
return "WaitingForSettings";
}
const char* liteWalletServerSelectionUiExecutionIssueName(
LiteWalletServerSelectionUiExecutionIssue issue)
{
switch (issue) {
case LiteWalletServerSelectionUiExecutionIssue::FullNodeBuild:
return "FullNodeBuild";
case LiteWalletServerSelectionUiExecutionIssue::LiteBackendCapabilityMissing:
return "LiteBackendCapabilityMissing";
case LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded:
return "SettingsNotLoaded";
case LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing:
return "ServerSelectionMissing";
case LiteWalletServerSelectionUiExecutionIssue::ServerPersistenceOwnerMissing:
return "ServerPersistenceOwnerMissing";
case LiteWalletServerSelectionUiExecutionIssue::SelectedServerDisplayMissing:
return "SelectedServerDisplayMissing";
case LiteWalletServerSelectionUiExecutionIssue::LifecycleUiOwnerMissing:
return "LifecycleUiOwnerMissing";
case LiteWalletServerSelectionUiExecutionIssue::PrivateDataRedactionMissing:
return "PrivateDataRedactionMissing";
case LiteWalletServerSelectionUiExecutionIssue::SyncPlannerFeedMissing:
return "SyncPlannerFeedMissing";
case LiteWalletServerSelectionUiExecutionIssue::SettingsSaveFailed:
return "SettingsSaveFailed";
case LiteWalletServerSelectionUiExecutionIssue::ServerConnectivityCheckRequested:
return "ServerConnectivityCheckRequested";
case LiteWalletServerSelectionUiExecutionIssue::WalletLifecycleExecutionRequested:
return "WalletLifecycleExecutionRequested";
case LiteWalletServerSelectionUiExecutionIssue::SyncRequested:
return "SyncRequested";
case LiteWalletServerSelectionUiExecutionIssue::SyncStatusPollingRequested:
return "SyncStatusPollingRequested";
case LiteWalletServerSelectionUiExecutionIssue::WorkerQueueRequested:
return "WorkerQueueRequested";
case LiteWalletServerSelectionUiExecutionIssue::WalletStateMutationRequested:
return "WalletStateMutationRequested";
case LiteWalletServerSelectionUiExecutionIssue::WalletFilePersistenceRequested:
return "WalletFilePersistenceRequested";
case LiteWalletServerSelectionUiExecutionIssue::SendImportExportRequested:
return "SendImportExportRequested";
}
return "SettingsNotLoaded";
}
std::string redactLiteServerSelectionValue(const std::string& value)
{
const std::string trimmed = trimCopy(value);
if (trimmed.empty()) return "<empty>";
const auto scheme = trimmed.find("://");
if (scheme == std::string::npos) return "<redacted>";
return trimmed.substr(0, scheme + 3) + "<redacted>";
}
LiteWalletServerSelectionUiExecutionResult executeLiteWalletServerSelectionUi(
config::Settings& settings,
const LiteWalletServerSelectionUiExecutionInput& input)
{
LiteWalletServerSelectionUiExecutionResult result;
auto stopForRuntime = [&](LiteWalletServerSelectionUiExecutionIssue issue,
const std::string& message) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::RuntimeExecutionDisabled,
issue,
message);
};
if (input.serverConnectivityCheckRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::ServerConnectivityCheckRequested,
"lite server selection settings adapter cannot check server connectivity");
}
if (input.walletExistsCheckRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::ServerConnectivityCheckRequested,
"lite server selection settings adapter cannot check wallet existence");
}
if (input.walletLifecycleExecutionRequested || input.ui.realLifecycleExecutionRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::WalletLifecycleExecutionRequested,
"lite server selection settings adapter cannot execute wallet lifecycle actions");
}
if (input.syncStartRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::SyncRequested,
"lite server selection settings adapter cannot start sync");
}
if (input.syncStatusPollingRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::SyncStatusPollingRequested,
"lite server selection settings adapter cannot poll syncstatus");
}
if (input.workerQueueEnqueueRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::WorkerQueueRequested,
"lite server selection settings adapter cannot enqueue wallet workers");
}
if (input.walletStateMutationRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::WalletStateMutationRequested,
"lite server selection settings adapter cannot mutate WalletState");
}
if (input.walletFilePersistenceRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::WalletFilePersistenceRequested,
"lite server selection settings adapter cannot persist wallet files");
}
if (input.sendImportExportExecutionRequested) {
return stopForRuntime(LiteWalletServerSelectionUiExecutionIssue::SendImportExportRequested,
"lite server selection settings adapter cannot execute send/import/export flows");
}
if (!input.persistence.settingsLoaded) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings,
LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded,
"lite server settings are not loaded");
}
result.connectionSettings = settingsWithIntent(
liteConnectionSettingsFromAppSettings(settings), input.intent);
if (input.intent.selectedServerIntentProvided &&
input.intent.selectionMode == LiteServerSelectionMode::Sticky &&
!isLiteServerUrlUsable(trimCopy(input.intent.selectedServerUrl))) {
result.selectedServerUrlRedacted = redactLiteServerSelectionValue(input.intent.selectedServerUrl);
result.selectedServerRedacted = true;
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection,
LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing,
"lite server selection URL is not usable");
}
result.selectedServer = selectLiteServer(result.connectionSettings);
result.selectedServerUrlRedacted = result.selectedServer.ok
? redactLiteServerSelectionValue(result.selectedServer.server.url)
: redactLiteServerSelectionValue(result.connectionSettings.stickyServerUrl);
result.selectedServerRedacted = true;
if (!result.selectedServer.ok) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::WaitingForServerSelection,
LiteWalletServerSelectionUiExecutionIssue::ServerSelectionMissing,
result.selectedServer.error.empty()
? "lite server selection is missing"
: result.selectedServer.error);
}
result.selectedServerIntentAccepted = true;
if ((input.persistence.persistSelectedServer || input.persistence.writeSettings) &&
!input.persistence.persistenceOwnerReady) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::WaitingForPersistenceOwner,
LiteWalletServerSelectionUiExecutionIssue::ServerPersistenceOwnerMissing,
"lite server settings persistence owner is not ready");
}
result.settingsPersistenceAccepted = true;
if (input.persistence.persistSelectedServer || input.persistence.writeSettings) {
applyLiteConnectionSettingsToAppSettings(settings, result.connectionSettings);
settings.setLitePersistSelectedServer(input.persistence.persistSelectedServer);
result.settingsMutated = true;
}
if (input.persistence.writeSettings) {
result.noSettingsPersistence = false;
const bool saved = input.persistence.settingsSavePath.empty()
? settings.save()
: settings.save(input.persistence.settingsSavePath);
if (!saved) {
return stoppedResult(std::move(result),
LiteWalletServerSelectionUiExecutionStatus::SettingsSaveFailed,
LiteWalletServerSelectionUiExecutionIssue::SettingsSaveFailed,
"failed to save lite server settings");
}
result.settingsWritten = true;
}
result.lifecycleInput = makeLifecycleInput(input, result.connectionSettings);
result.lifecycleReadiness = evaluateLiteWalletServerLifecycleReadiness(result.lifecycleInput);
result.lifecycleReadinessAccepted = result.lifecycleReadiness.ok;
result.lifecycleReportCouldFeedSyncPlanners = result.lifecycleReadiness.lifecycleReportCouldFeedSyncPlanners;
result.status = statusFromLifecycle(result.lifecycleReadiness.status);
copyLifecycleIssues(result);
result.diagnosticSummary = buildDiagnosticSummary(result);
if (input.requireLifecycleReadiness && !result.lifecycleReadiness.ok) {
result.ok = false;
result.error = result.lifecycleReadiness.error;
return result;
}
result.ok = true;
return result;
}
LiteWalletServerSelectionUiExecutionAdapter::LiteWalletServerSelectionUiExecutionAdapter(
config::Settings& settings)
: settings_(settings)
{
}
LiteWalletServerSelectionUiExecutionResult LiteWalletServerSelectionUiExecutionAdapter::execute(
const LiteWalletServerSelectionUiExecutionInput& input) const
{
return executeLiteWalletServerSelectionUi(settings_, input);
}
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,161 @@
#pragma once
#include "lite_connection_service.h"
#include "lite_wallet_server_lifecycle_readiness.h"
#include "wallet_capabilities.h"
#include "../config/settings.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx {
namespace wallet {
enum class LiteWalletServerSelectionUiExecutionStatus {
ReadyForFutureLifecycle,
WaitingForLiteBuild,
WaitingForBackendCapability,
WaitingForSettings,
WaitingForServerSelection,
WaitingForPersistenceOwner,
WaitingForDisplayStatus,
WaitingForLifecycleUi,
WaitingForPrivateDataRedaction,
WaitingForSyncPlannerFeed,
SettingsSaveFailed,
RuntimeExecutionDisabled
};
enum class LiteWalletServerSelectionUiExecutionIssue {
FullNodeBuild,
LiteBackendCapabilityMissing,
SettingsNotLoaded,
ServerSelectionMissing,
ServerPersistenceOwnerMissing,
SelectedServerDisplayMissing,
LifecycleUiOwnerMissing,
PrivateDataRedactionMissing,
SyncPlannerFeedMissing,
SettingsSaveFailed,
ServerConnectivityCheckRequested,
WalletLifecycleExecutionRequested,
SyncRequested,
SyncStatusPollingRequested,
WorkerQueueRequested,
WalletStateMutationRequested,
WalletFilePersistenceRequested,
SendImportExportRequested
};
struct LiteWalletServerSelectionUiIntent {
bool selectedServerIntentProvided = false;
LiteServerSelectionMode selectionMode = LiteServerSelectionMode::Sticky;
std::string selectedServerUrl;
std::size_t randomSelectionSeed = 0;
std::string chainName;
bool replaceServers = false;
std::vector<LiteServerEndpoint> servers;
};
struct LiteWalletServerSelectionPersistenceExecutionInput {
bool settingsLoaded = true;
bool havePersistedSelectionIntent = true;
bool persistSelectedServer = true;
bool persistenceOwnerReady = true;
bool writeSettings = false;
std::string settingsSavePath;
};
struct LiteWalletServerSelectionUiExecutionInput {
WalletCapabilities capabilities;
LiteWalletServerSelectionUiIntent intent;
LiteWalletServerSelectionPersistenceExecutionInput persistence;
LiteWalletLifecycleUiPrerequisites ui;
LiteWalletLifecycleOperation operation = LiteWalletLifecycleOperation::CreateNew;
LiteWalletCreateRequest createRequest;
LiteWalletOpenRequest openRequest;
LiteWalletRestoreRequest restoreRequest;
bool requireLifecycleReadiness = false;
bool serverConnectivityCheckRequested = false;
bool walletExistsCheckRequested = false;
bool walletLifecycleExecutionRequested = false;
bool syncStartRequested = false;
bool syncStatusPollingRequested = false;
bool workerQueueEnqueueRequested = false;
bool walletStateMutationRequested = false;
bool walletFilePersistenceRequested = false;
bool sendImportExportExecutionRequested = false;
};
struct LiteWalletServerSelectionUiExecutionIssueInfo {
LiteWalletServerSelectionUiExecutionIssue issue = LiteWalletServerSelectionUiExecutionIssue::SettingsNotLoaded;
std::string message;
};
struct LiteWalletServerSelectionUiExecutionResult {
bool ok = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool noBridgeCalls = true;
bool noServerHealthChecked = true;
bool noWalletExistsChecked = true;
bool noWalletCreated = true;
bool noWalletOpened = true;
bool noWalletRestored = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWalletPersistence = true;
bool noSendImportExportExecution = true;
bool workerQueueEnqueued = false;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool settingsMutationAllowed = true;
bool settingsMutated = false;
bool noSettingsPersistence = true;
bool settingsWritten = false;
bool settingsPersistenceAccepted = false;
bool selectedServerIntentAccepted = false;
bool lifecycleReadinessAccepted = false;
bool lifecycleReportCouldFeedSyncPlanners = false;
bool selectedServerRedacted = false;
LiteWalletServerSelectionUiExecutionStatus status = LiteWalletServerSelectionUiExecutionStatus::WaitingForSettings;
std::string error;
std::string selectedServerUrlRedacted;
std::string diagnosticSummary;
LiteConnectionSettings connectionSettings;
LiteServerSelectionResult selectedServer;
LiteWalletServerLifecycleReadinessInput lifecycleInput;
LiteWalletServerLifecycleReadinessResult lifecycleReadiness;
std::vector<LiteWalletServerSelectionUiExecutionIssueInfo> issues;
};
LiteConnectionSettings liteConnectionSettingsFromAppSettings(const config::Settings& settings);
void applyLiteConnectionSettingsToAppSettings(config::Settings& settings,
const LiteConnectionSettings& connectionSettings);
const char* liteWalletServerSelectionUiExecutionStatusName(
LiteWalletServerSelectionUiExecutionStatus status);
const char* liteWalletServerSelectionUiExecutionIssueName(
LiteWalletServerSelectionUiExecutionIssue issue);
std::string redactLiteServerSelectionValue(const std::string& value);
LiteWalletServerSelectionUiExecutionResult executeLiteWalletServerSelectionUi(
config::Settings& settings,
const LiteWalletServerSelectionUiExecutionInput& input);
class LiteWalletServerSelectionUiExecutionAdapter {
public:
explicit LiteWalletServerSelectionUiExecutionAdapter(config::Settings& settings);
LiteWalletServerSelectionUiExecutionResult execute(
const LiteWalletServerSelectionUiExecutionInput& input) const;
private:
config::Settings& settings_;
};
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,118 @@
#include "wallet/lite_wallet_state_apply_executor.h"
#include <utility>
namespace dragonx::wallet {
namespace {
void addIssue(LiteWalletStateApplyExecutionResult& result,
LiteWalletStateApplyExecutionIssue issue,
std::string message)
{
result.issues.push_back(LiteWalletStateApplyExecutionIssueInfo{issue, std::move(message)});
}
std::size_t countPlannedChanges(const LiteWalletStateApplyPlan& plan)
{
std::size_t count = 0;
for (const auto& fieldPlan : plan.fieldPlans) {
if (fieldPlan.wouldChange) ++count;
}
for (const auto& collectionPlan : plan.collectionPlans) {
if (collectionPlan.wouldReplace) ++count;
}
return count;
}
LiteWalletStateApplyExecutionResult baseResult(const LiteWalletStateApplyPlan& plan,
LiteWalletStateApplyExecutorOptions options)
{
LiteWalletStateApplyExecutionResult result;
result.stateMutationRequested = options.requestStateMutation;
result.stateMutationAllowed = false;
result.applyImplemented = false;
result.fieldPlanCount = plan.fieldPlans.size();
result.collectionPlanCount = plan.collectionPlans.size();
result.plannedChangeCount = countPlannedChanges(plan);
result.planIssueCount = plan.issues.size();
return result;
}
} // namespace
const char* liteWalletStateApplyExecutionStatusName(LiteWalletStateApplyExecutionStatus status)
{
switch (status) {
case LiteWalletStateApplyExecutionStatus::Disabled: return "Disabled";
case LiteWalletStateApplyExecutionStatus::Rejected: return "Rejected";
case LiteWalletStateApplyExecutionStatus::ImplementationMissing: return "ImplementationMissing";
}
return "Unknown";
}
const char* liteWalletStateApplyExecutionIssueName(LiteWalletStateApplyExecutionIssue issue)
{
switch (issue) {
case LiteWalletStateApplyExecutionIssue::InvalidPlan: return "InvalidPlan";
case LiteWalletStateApplyExecutionIssue::MutablePlanRejected: return "MutablePlanRejected";
case LiteWalletStateApplyExecutionIssue::StateMutationDisabled: return "StateMutationDisabled";
case LiteWalletStateApplyExecutionIssue::StateMutationImplementationMissing: return "StateMutationImplementationMissing";
}
return "Unknown";
}
LiteWalletStateApplyExecutionResult executeLiteWalletStateApplyPlan(
const LiteWalletStateApplyPlan& plan,
LiteWalletStateApplyExecutorOptions options)
{
auto result = baseResult(plan, options);
if (!plan.ok) {
result.status = LiteWalletStateApplyExecutionStatus::Rejected;
addIssue(result,
LiteWalletStateApplyExecutionIssue::InvalidPlan,
"lite WalletState apply plan is not valid");
result.error = plan.error.empty() ? result.issues.back().message : plan.error;
return result;
}
if (!plan.dryRunOnly || plan.applyImplemented || plan.stateMutationAllowed) {
result.status = LiteWalletStateApplyExecutionStatus::Rejected;
addIssue(result,
LiteWalletStateApplyExecutionIssue::MutablePlanRejected,
"lite WalletState apply executor accepts dry-run-only plans only");
result.error = result.issues.back().message;
return result;
}
result.planAccepted = true;
if (options.requestStateMutation) {
result.status = LiteWalletStateApplyExecutionStatus::ImplementationMissing;
addIssue(result,
LiteWalletStateApplyExecutionIssue::StateMutationImplementationMissing,
"real lite WalletState application requires a future explicit implementation");
result.error = result.issues.back().message;
return result;
}
result.ok = true;
result.status = LiteWalletStateApplyExecutionStatus::Disabled;
addIssue(result,
LiteWalletStateApplyExecutionIssue::StateMutationDisabled,
"lite WalletState application is disabled; dry-run plan was accepted for reporting only");
return result;
}
LiteWalletStateApplyExecutor::LiteWalletStateApplyExecutor(LiteWalletStateApplyExecutorOptions options)
: options_(options)
{
}
LiteWalletStateApplyExecutionResult LiteWalletStateApplyExecutor::execute(
const LiteWalletStateApplyPlan& plan) const
{
return executeLiteWalletStateApplyPlan(plan, options_);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,71 @@
#pragma once
#include "lite_wallet_state_apply_plan.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletStateApplyExecutionStatus {
Disabled,
Rejected,
ImplementationMissing,
};
enum class LiteWalletStateApplyExecutionIssue {
InvalidPlan,
MutablePlanRejected,
StateMutationDisabled,
StateMutationImplementationMissing,
};
struct LiteWalletStateApplyExecutorOptions {
bool requestStateMutation = false;
};
struct LiteWalletStateApplyExecutionIssueInfo {
LiteWalletStateApplyExecutionIssue issue = LiteWalletStateApplyExecutionIssue::StateMutationDisabled;
std::string message;
};
struct LiteWalletStateApplyExecutionResult {
bool ok = false;
bool planAccepted = false;
bool attempted = false;
bool applied = false;
bool stateMutated = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool stateMutationRequested = false;
bool stateMutationAllowed = false;
bool applyImplemented = false;
LiteWalletStateApplyExecutionStatus status = LiteWalletStateApplyExecutionStatus::Disabled;
std::size_t fieldPlanCount = 0;
std::size_t collectionPlanCount = 0;
std::size_t plannedChangeCount = 0;
std::size_t planIssueCount = 0;
std::vector<LiteWalletStateApplyExecutionIssueInfo> issues;
std::string error;
};
const char* liteWalletStateApplyExecutionStatusName(LiteWalletStateApplyExecutionStatus status);
const char* liteWalletStateApplyExecutionIssueName(LiteWalletStateApplyExecutionIssue issue);
LiteWalletStateApplyExecutionResult executeLiteWalletStateApplyPlan(
const LiteWalletStateApplyPlan& plan,
LiteWalletStateApplyExecutorOptions options = {});
class LiteWalletStateApplyExecutor {
public:
explicit LiteWalletStateApplyExecutor(LiteWalletStateApplyExecutorOptions options = {});
LiteWalletStateApplyExecutionResult execute(const LiteWalletStateApplyPlan& plan) const;
private:
LiteWalletStateApplyExecutorOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,405 @@
#include "wallet/lite_wallet_state_apply_plan.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <sstream>
#include <utility>
namespace dragonx::wallet {
namespace {
constexpr double kZatoshisPerCoin = 100000000.0;
std::string stringValue(const std::string& value)
{
return value;
}
std::string intValue(std::int64_t value)
{
return std::to_string(value);
}
std::string uintValue(std::uint64_t value)
{
return std::to_string(value);
}
std::string doubleValue(double value)
{
std::ostringstream output;
output << value;
return output.str();
}
std::string boolValue(bool value)
{
return value ? "true" : "false";
}
std::uint64_t coinsToZatoshis(double coins)
{
if (coins <= 0.0) return 0;
const double zatoshis = std::round(coins * kZatoshisPerCoin);
const double maxValue = static_cast<double>(std::numeric_limits<std::uint64_t>::max());
if (zatoshis >= maxValue) return std::numeric_limits<std::uint64_t>::max();
return static_cast<std::uint64_t>(zatoshis);
}
bool modelHasAnySection(const LiteWalletAppRefreshModel& model)
{
return model.hasChainInfo || model.hasHeight || model.hasBalance || model.hasAddresses ||
model.hasSpendableOutputs || model.hasTransactions || model.hasSyncStatus;
}
void addIssue(LiteWalletStateApplyPlan& plan,
LiteWalletStateApplyIssue issue,
std::string message)
{
plan.issues.push_back(LiteWalletStateApplyIssueInfo{issue, std::move(message)});
}
void addFieldPlan(LiteWalletStateApplyPlan& plan,
LiteWalletStateApplySection section,
const std::string& fieldName,
std::string currentValue,
std::string proposedValue)
{
LiteWalletStateApplyFieldPlan fieldPlan;
fieldPlan.section = section;
fieldPlan.fieldName = fieldName;
fieldPlan.currentValue = std::move(currentValue);
fieldPlan.proposedValue = std::move(proposedValue);
fieldPlan.wouldChange = fieldPlan.currentValue != fieldPlan.proposedValue;
fieldPlan.action = fieldPlan.wouldChange
? LiteWalletStateApplyAction::SetField
: LiteWalletStateApplyAction::Noop;
if (fieldPlan.wouldChange) plan.wouldChangeWalletState = true;
plan.fieldPlans.push_back(std::move(fieldPlan));
}
void addCollectionPlan(LiteWalletStateApplyPlan& plan,
LiteWalletStateApplySection section,
std::string collectionName,
std::size_t currentCount,
std::size_t proposedCount,
bool hasWalletStateTarget,
bool wouldReplace)
{
LiteWalletStateApplyCollectionPlan collectionPlan;
collectionPlan.section = section;
collectionPlan.collectionName = std::move(collectionName);
collectionPlan.currentCount = currentCount;
collectionPlan.proposedCount = proposedCount;
collectionPlan.hasWalletStateTarget = hasWalletStateTarget;
collectionPlan.wouldReplace = hasWalletStateTarget && wouldReplace;
if (!hasWalletStateTarget) {
collectionPlan.action = LiteWalletStateApplyAction::InspectOnly;
} else {
collectionPlan.action = collectionPlan.wouldReplace
? LiteWalletStateApplyAction::ReplaceCollection
: LiteWalletStateApplyAction::Noop;
}
if (collectionPlan.wouldReplace) plan.wouldChangeWalletState = true;
plan.collectionPlans.push_back(std::move(collectionPlan));
}
std::vector<std::string> mappedAddresses(const LiteWalletAppRefreshModel& model,
LiteWalletAppAddressKind kind)
{
std::vector<std::string> addresses;
for (const auto& address : model.addresses) {
if (address.kind == kind) addresses.push_back(address.address);
}
return addresses;
}
std::vector<std::string> mappedAddressStrings(const LiteWalletAppRefreshModel& model)
{
std::vector<std::string> addresses;
addresses.reserve(model.addresses.size());
for (const auto& address : model.addresses) addresses.push_back(address.address);
return addresses;
}
std::vector<std::string> stateAddressStrings(const std::vector<dragonx::AddressInfo>& addresses)
{
std::vector<std::string> values;
values.reserve(addresses.size());
for (const auto& address : addresses) values.push_back(address.address);
return values;
}
std::vector<std::string> mappedTransactionIds(const LiteWalletAppRefreshModel& model)
{
std::vector<std::string> txids;
txids.reserve(model.transactions.size());
for (const auto& transaction : model.transactions) txids.push_back(transaction.txid);
return txids;
}
std::vector<std::string> stateTransactionIds(const std::vector<dragonx::TransactionInfo>& transactions)
{
std::vector<std::string> txids;
txids.reserve(transactions.size());
for (const auto& transaction : transactions) txids.push_back(transaction.txid);
return txids;
}
void addChainInfoPlans(LiteWalletStateApplyPlan& plan,
const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state)
{
if (!model.hasChainInfo) return;
plan.hasChainInfo = true;
if (model.chain.longestChain) {
addFieldPlan(plan,
LiteWalletStateApplySection::ChainInfo,
"longestchain",
intValue(state.longestchain),
intValue(*model.chain.longestChain));
}
if (model.chain.notarized) {
addFieldPlan(plan,
LiteWalletStateApplySection::ChainInfo,
"notarized",
intValue(state.notarized),
intValue(*model.chain.notarized));
}
if (model.chain.chainName) {
addFieldPlan(plan,
LiteWalletStateApplySection::ChainInfo,
"chain_name",
stringValue(state.mining.chain),
stringValue(*model.chain.chainName));
}
}
void addHeightPlans(LiteWalletStateApplyPlan& plan,
const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state)
{
if (!model.hasHeight) return;
plan.hasHeight = true;
addFieldPlan(plan,
LiteWalletStateApplySection::Height,
"sync.blocks",
intValue(state.sync.blocks),
intValue(model.height.height));
}
void addBalancePlans(LiteWalletStateApplyPlan& plan,
const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state)
{
if (!model.hasBalance) return;
plan.hasBalance = true;
addFieldPlan(plan,
LiteWalletStateApplySection::Balance,
"privateBalanceZatoshis",
uintValue(coinsToZatoshis(state.privateBalance)),
uintValue(model.balance.shieldedZatoshis));
addFieldPlan(plan,
LiteWalletStateApplySection::Balance,
"transparentBalanceZatoshis",
uintValue(coinsToZatoshis(state.transparentBalance)),
uintValue(model.balance.transparentZatoshis));
addFieldPlan(plan,
LiteWalletStateApplySection::Balance,
"totalBalanceZatoshis",
uintValue(coinsToZatoshis(state.totalBalance)),
uintValue(model.balance.totalZatoshis));
addFieldPlan(plan,
LiteWalletStateApplySection::Balance,
"unconfirmedBalanceZatoshis",
uintValue(coinsToZatoshis(state.unconfirmedBalance)),
uintValue(model.balance.unconfirmedZatoshis));
}
void addAddressPlans(LiteWalletStateApplyPlan& plan,
const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state)
{
if (!model.hasAddresses) return;
plan.hasAddresses = true;
const auto proposedShielded = mappedAddresses(model, LiteWalletAppAddressKind::Shielded);
const auto proposedTransparent = mappedAddresses(model, LiteWalletAppAddressKind::Transparent);
const auto proposedCombined = mappedAddressStrings(model);
const auto currentShielded = stateAddressStrings(state.z_addresses);
const auto currentTransparent = stateAddressStrings(state.t_addresses);
const auto currentCombined = stateAddressStrings(state.addresses);
addCollectionPlan(plan,
LiteWalletStateApplySection::Addresses,
"z_addresses",
currentShielded.size(),
proposedShielded.size(),
true,
currentShielded != proposedShielded);
addCollectionPlan(plan,
LiteWalletStateApplySection::Addresses,
"t_addresses",
currentTransparent.size(),
proposedTransparent.size(),
true,
currentTransparent != proposedTransparent);
addCollectionPlan(plan,
LiteWalletStateApplySection::Addresses,
"addresses",
currentCombined.size(),
proposedCombined.size(),
true,
currentCombined != proposedCombined);
const auto unknownSpendability = std::any_of(model.addresses.begin(), model.addresses.end(),
[](const LiteWalletAppAddressModel& address) { return !address.spendabilityKnown; });
if (unknownSpendability) {
addIssue(plan,
LiteWalletStateApplyIssue::AddressSpendabilityUnknown,
"lite address rows do not yet carry wallet spendability policy");
}
}
void addSpendableOutputPlans(LiteWalletStateApplyPlan& plan,
const LiteWalletAppRefreshModel& model)
{
if (!model.hasSpendableOutputs) return;
plan.hasSpendableOutputs = true;
addCollectionPlan(plan,
LiteWalletStateApplySection::SpendableOutputs,
"spendableOutputs",
0,
model.spendableOutputs.size(),
false,
false);
addIssue(plan,
LiteWalletStateApplyIssue::SpendableOutputsHaveNoWalletStateTarget,
"lite spendable outputs remain source-only until an app apply target is designed");
}
void addTransactionPlans(LiteWalletStateApplyPlan& plan,
const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state)
{
if (!model.hasTransactions) return;
plan.hasTransactions = true;
const auto proposedTxids = mappedTransactionIds(model);
const auto currentTxids = stateTransactionIds(state.transactions);
addCollectionPlan(plan,
LiteWalletStateApplySection::Transactions,
"transactions",
currentTxids.size(),
proposedTxids.size(),
true,
currentTxids != proposedTxids);
}
void addSyncPlans(LiteWalletStateApplyPlan& plan,
const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state)
{
if (!model.hasSyncStatus) return;
plan.hasSyncStatus = true;
addFieldPlan(plan,
LiteWalletStateApplySection::SyncStatus,
"sync.blocks",
intValue(state.sync.blocks),
uintValue(model.sync.walletHeight));
addFieldPlan(plan,
LiteWalletStateApplySection::SyncStatus,
"sync.headers",
intValue(state.sync.headers),
uintValue(model.sync.chainHeight));
addFieldPlan(plan,
LiteWalletStateApplySection::SyncStatus,
"sync.verification_progress",
doubleValue(state.sync.verification_progress),
doubleValue(model.sync.progress));
addFieldPlan(plan,
LiteWalletStateApplySection::SyncStatus,
"sync.syncing",
boolValue(state.sync.syncing),
boolValue(!model.sync.complete));
}
} // namespace
const char* liteWalletStateApplySectionName(LiteWalletStateApplySection section)
{
switch (section) {
case LiteWalletStateApplySection::ChainInfo: return "ChainInfo";
case LiteWalletStateApplySection::Height: return "Height";
case LiteWalletStateApplySection::Balance: return "Balance";
case LiteWalletStateApplySection::Addresses: return "Addresses";
case LiteWalletStateApplySection::SpendableOutputs: return "SpendableOutputs";
case LiteWalletStateApplySection::Transactions: return "Transactions";
case LiteWalletStateApplySection::SyncStatus: return "SyncStatus";
}
return "Unknown";
}
const char* liteWalletStateApplyActionName(LiteWalletStateApplyAction action)
{
switch (action) {
case LiteWalletStateApplyAction::Noop: return "Noop";
case LiteWalletStateApplyAction::SetField: return "SetField";
case LiteWalletStateApplyAction::ReplaceCollection: return "ReplaceCollection";
case LiteWalletStateApplyAction::InspectOnly: return "InspectOnly";
}
return "Unknown";
}
const char* liteWalletStateApplyIssueName(LiteWalletStateApplyIssue issue)
{
switch (issue) {
case LiteWalletStateApplyIssue::EmptyModel: return "EmptyModel";
case LiteWalletStateApplyIssue::IncompleteModel: return "IncompleteModel";
case LiteWalletStateApplyIssue::AddressSpendabilityUnknown: return "AddressSpendabilityUnknown";
case LiteWalletStateApplyIssue::SpendableOutputsHaveNoWalletStateTarget: return "SpendableOutputsHaveNoWalletStateTarget";
}
return "Unknown";
}
LiteWalletStateApplyPlan planLiteWalletStateApply(const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state)
{
LiteWalletStateApplyPlan plan;
plan.sourceComplete = model.complete;
if (!modelHasAnySection(model)) {
addIssue(plan,
LiteWalletStateApplyIssue::EmptyModel,
"lite app refresh model has no sections to compare");
plan.error = plan.issues.back().message;
return plan;
}
if (!model.complete) {
addIssue(plan,
LiteWalletStateApplyIssue::IncompleteModel,
"lite app refresh model is partial; dry-run apply planning only");
}
addChainInfoPlans(plan, model, state);
addHeightPlans(plan, model, state);
addBalancePlans(plan, model, state);
addAddressPlans(plan, model, state);
addSpendableOutputPlans(plan, model);
addTransactionPlans(plan, model, state);
addSyncPlans(plan, model, state);
plan.ok = true;
return plan;
}
LiteWalletStateApplyPlan LiteWalletStateApplyPlanner::buildPlan(
const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state) const
{
return planLiteWalletStateApply(model, state);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,98 @@
#pragma once
#include "data/wallet_state.h"
#include "lite_wallet_state_mapper.h"
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletStateApplySection {
ChainInfo,
Height,
Balance,
Addresses,
SpendableOutputs,
Transactions,
SyncStatus,
};
enum class LiteWalletStateApplyAction {
Noop,
SetField,
ReplaceCollection,
InspectOnly,
};
enum class LiteWalletStateApplyIssue {
EmptyModel,
IncompleteModel,
AddressSpendabilityUnknown,
SpendableOutputsHaveNoWalletStateTarget,
};
struct LiteWalletStateApplyIssueInfo {
LiteWalletStateApplyIssue issue = LiteWalletStateApplyIssue::EmptyModel;
std::string message;
};
struct LiteWalletStateApplyFieldPlan {
LiteWalletStateApplySection section = LiteWalletStateApplySection::Balance;
LiteWalletStateApplyAction action = LiteWalletStateApplyAction::Noop;
std::string fieldName;
std::string currentValue;
std::string proposedValue;
bool currentKnown = true;
bool proposedKnown = true;
bool wouldChange = false;
};
struct LiteWalletStateApplyCollectionPlan {
LiteWalletStateApplySection section = LiteWalletStateApplySection::Addresses;
LiteWalletStateApplyAction action = LiteWalletStateApplyAction::Noop;
std::string collectionName;
std::size_t currentCount = 0;
std::size_t proposedCount = 0;
bool hasWalletStateTarget = true;
bool wouldReplace = false;
};
struct LiteWalletStateApplyPlan {
bool ok = false;
bool dryRunOnly = true;
bool applyImplemented = false;
bool stateMutationAllowed = false;
bool wouldChangeWalletState = false;
bool sourceComplete = false;
bool hasChainInfo = false;
bool hasHeight = false;
bool hasBalance = false;
bool hasAddresses = false;
bool hasSpendableOutputs = false;
bool hasTransactions = false;
bool hasSyncStatus = false;
std::vector<LiteWalletStateApplyFieldPlan> fieldPlans;
std::vector<LiteWalletStateApplyCollectionPlan> collectionPlans;
std::vector<LiteWalletStateApplyIssueInfo> issues;
std::string error;
};
const char* liteWalletStateApplySectionName(LiteWalletStateApplySection section);
const char* liteWalletStateApplyActionName(LiteWalletStateApplyAction action);
const char* liteWalletStateApplyIssueName(LiteWalletStateApplyIssue issue);
LiteWalletStateApplyPlan planLiteWalletStateApply(const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state);
class LiteWalletStateApplyPlanner {
public:
LiteWalletStateApplyPlan buildPlan(const LiteWalletAppRefreshModel& model,
const dragonx::WalletState& state) const;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,217 @@
#include "wallet/lite_wallet_state_mapper.h"
#include <utility>
namespace dragonx::wallet {
namespace {
LiteWalletAppAddressModel mapAddress(const std::string& address, LiteWalletAppAddressKind kind)
{
LiteWalletAppAddressModel model;
model.address = address;
model.kind = kind;
return model;
}
LiteWalletAppSpendableOutputModel mapSpendableOutput(const LiteSpendableOutput& output)
{
LiteWalletAppSpendableOutputModel model;
model.kind = output.kind;
model.address = output.address;
model.createdInTxid = output.createdInTxid;
model.createdInBlock = output.createdInBlock;
model.valueZatoshis = output.value;
model.spent = output.spent;
model.unconfirmedSpent = output.unconfirmedSpent;
model.pending = output.pending;
model.spendable = output.spendable;
return model;
}
LiteWalletAppTransactionKind mapTransactionKind(LiteTransactionDirection direction)
{
switch (direction) {
case LiteTransactionDirection::Send: return LiteWalletAppTransactionKind::Send;
case LiteTransactionDirection::Receive: return LiteWalletAppTransactionKind::Receive;
case LiteTransactionDirection::Unknown: return LiteWalletAppTransactionKind::Unknown;
}
return LiteWalletAppTransactionKind::Unknown;
}
LiteWalletAppTransactionOutputModel mapTransactionOutput(const LiteTransactionOutput& output)
{
LiteWalletAppTransactionOutputModel model;
model.address = output.address;
model.valueZatoshis = output.value;
model.memo = output.memo;
return model;
}
LiteWalletAppTransactionModel mapTransaction(const LiteTransactionRecord& transaction)
{
LiteWalletAppTransactionModel model;
model.txid = transaction.txid;
model.kind = mapTransactionKind(transaction.direction);
model.timestamp = transaction.datetime;
model.blockHeight = transaction.blockHeight;
model.unconfirmed = transaction.unconfirmed;
model.address = transaction.address;
model.amountZatoshis = transaction.amount;
model.signedAmountZatoshis = model.kind == LiteWalletAppTransactionKind::Send
? -transaction.amount
: transaction.amount;
model.memo = transaction.memo;
model.position = transaction.position;
model.outgoingOutputs.reserve(transaction.outgoingMetadata.size());
for (const auto& output : transaction.outgoingMetadata) {
model.outgoingOutputs.push_back(mapTransactionOutput(output));
}
return model;
}
bool modelHasAnyMappedField(const LiteWalletAppRefreshModel& model)
{
return model.hasChainInfo || model.hasHeight || model.hasBalance || model.hasAddresses ||
model.hasSpendableOutputs || model.hasTransactions || model.hasSyncStatus;
}
void addIssue(LiteWalletStateMapResult& result, LiteWalletStateMapIssue issue, std::string message)
{
result.issues.push_back(LiteWalletStateMapIssueInfo{issue, std::move(message)});
}
} // namespace
const char* liteWalletAppAddressKindName(LiteWalletAppAddressKind kind)
{
switch (kind) {
case LiteWalletAppAddressKind::Shielded: return "shielded";
case LiteWalletAppAddressKind::Transparent: return "transparent";
}
return "unknown";
}
const char* liteWalletAppTransactionKindName(LiteWalletAppTransactionKind kind)
{
switch (kind) {
case LiteWalletAppTransactionKind::Unknown: return "unknown";
case LiteWalletAppTransactionKind::Send: return "send";
case LiteWalletAppTransactionKind::Receive: return "receive";
}
return "unknown";
}
const char* liteWalletStateMapIssueName(LiteWalletStateMapIssue issue)
{
switch (issue) {
case LiteWalletStateMapIssue::EmptyBundle: return "EmptyBundle";
case LiteWalletStateMapIssue::IncompleteBundle: return "IncompleteBundle";
}
return "Unknown";
}
LiteWalletStateMapResult mapLiteWalletRefreshBundle(const LiteWalletRefreshBundle& bundle)
{
LiteWalletStateMapResult result;
result.stateMutationAllowed = false;
result.model.complete = bundle.complete;
result.model.successfulCommandCount = bundle.successfulCommandCount;
if (bundle.hasInfo) {
result.model.hasChainInfo = true;
result.model.chain.chainName = bundle.info.chainName;
result.model.chain.version = bundle.info.version;
result.model.chain.vendor = bundle.info.vendor;
result.model.chain.latestBlockHeight = bundle.info.latestBlockHeight;
result.model.chain.difficulty = bundle.info.difficulty;
result.model.chain.longestChain = bundle.info.longestChain;
result.model.chain.notarized = bundle.info.notarized;
}
if (bundle.hasHeight) {
result.model.hasHeight = true;
result.model.height.height = bundle.height.height;
}
if (bundle.hasBalance) {
result.model.hasBalance = true;
result.model.balance.transparentZatoshis = bundle.balance.transparentBalance;
result.model.balance.shieldedZatoshis = bundle.balance.shieldedBalance;
result.model.balance.unconfirmedZatoshis = bundle.balance.unconfirmedBalance;
result.model.balance.verifiedShieldedZatoshis = bundle.balance.verifiedShieldedBalance;
result.model.balance.spendableShieldedZatoshis = bundle.balance.spendableShieldedBalance;
result.model.balance.totalZatoshis = bundle.balance.transparentBalance + bundle.balance.shieldedBalance;
}
if (bundle.hasAddresses) {
result.model.hasAddresses = true;
result.model.addresses.reserve(bundle.addresses.zAddresses.size() + bundle.addresses.tAddresses.size());
for (const auto& address : bundle.addresses.zAddresses) {
result.model.addresses.push_back(mapAddress(address, LiteWalletAppAddressKind::Shielded));
}
for (const auto& address : bundle.addresses.tAddresses) {
result.model.addresses.push_back(mapAddress(address, LiteWalletAppAddressKind::Transparent));
}
}
if (bundle.hasNotes) {
result.model.hasSpendableOutputs = true;
const auto outputCount = bundle.notes.unspentNotes.size() + bundle.notes.utxos.size() +
bundle.notes.pendingNotes.size() + bundle.notes.pendingUtxos.size();
result.model.spendableOutputs.reserve(outputCount);
for (const auto& output : bundle.notes.unspentNotes) {
result.model.spendableOutputs.push_back(mapSpendableOutput(output));
}
for (const auto& output : bundle.notes.utxos) {
result.model.spendableOutputs.push_back(mapSpendableOutput(output));
}
for (const auto& output : bundle.notes.pendingNotes) {
result.model.spendableOutputs.push_back(mapSpendableOutput(output));
}
for (const auto& output : bundle.notes.pendingUtxos) {
result.model.spendableOutputs.push_back(mapSpendableOutput(output));
}
}
if (bundle.hasTransactions) {
result.model.hasTransactions = true;
result.model.transactions.reserve(bundle.transactions.transactions.size());
for (const auto& transaction : bundle.transactions.transactions) {
result.model.transactions.push_back(mapTransaction(transaction));
}
}
if (bundle.hasSyncStatus) {
result.model.hasSyncStatus = true;
result.model.sync.walletHeight = bundle.syncStatus.syncedBlocks;
result.model.sync.chainHeight = bundle.syncStatus.totalBlocks;
result.model.sync.progress = bundle.syncStatus.progress;
result.model.sync.complete = bundle.syncStatus.complete;
}
if (!modelHasAnyMappedField(result.model)) {
addIssue(result, LiteWalletStateMapIssue::EmptyBundle, "lite refresh bundle has no mappable fields");
result.error = result.issues.back().message;
return result;
}
if (!bundle.complete) {
addIssue(result, LiteWalletStateMapIssue::IncompleteBundle, "lite refresh bundle is partial");
}
result.ok = true;
return result;
}
LiteWalletStateMapResult mapLiteWalletRefreshResult(const LiteWalletRefreshResult& result)
{
return mapLiteWalletRefreshBundle(result.bundle);
}
LiteWalletStateMapResult mapLiteWalletRefreshServiceResult(const LiteWalletRefreshServiceResult& result)
{
return mapLiteWalletRefreshBundle(result.bundle);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,139 @@
#pragma once
#include "lite_wallet_refresh_service.h"
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletAppAddressKind {
Shielded,
Transparent,
};
enum class LiteWalletAppTransactionKind {
Unknown,
Send,
Receive,
};
enum class LiteWalletStateMapIssue {
EmptyBundle,
IncompleteBundle,
};
struct LiteWalletAppChainModel {
std::optional<std::string> chainName;
std::optional<std::string> version;
std::optional<std::string> vendor;
std::optional<std::int64_t> latestBlockHeight;
std::optional<std::int64_t> difficulty;
std::optional<std::int64_t> longestChain;
std::optional<std::int64_t> notarized;
};
struct LiteWalletAppHeightModel {
std::int64_t height = 0;
};
struct LiteWalletAppBalanceModel {
std::uint64_t transparentZatoshis = 0;
std::uint64_t shieldedZatoshis = 0;
std::uint64_t unconfirmedZatoshis = 0;
std::uint64_t verifiedShieldedZatoshis = 0;
std::uint64_t spendableShieldedZatoshis = 0;
std::uint64_t totalZatoshis = 0;
};
struct LiteWalletAppAddressModel {
std::string address;
LiteWalletAppAddressKind kind = LiteWalletAppAddressKind::Shielded;
bool spendabilityKnown = false;
bool spendable = false;
};
struct LiteWalletAppSpendableOutputModel {
LiteSpendableOutputKind kind = LiteSpendableOutputKind::UnspentNote;
std::string address;
std::string createdInTxid;
std::optional<std::int64_t> createdInBlock;
std::uint64_t valueZatoshis = 0;
bool spent = false;
bool unconfirmedSpent = false;
bool pending = false;
bool spendable = false;
};
struct LiteWalletAppTransactionOutputModel {
std::string address;
std::int64_t valueZatoshis = 0;
std::string memo;
};
struct LiteWalletAppTransactionModel {
std::string txid;
LiteWalletAppTransactionKind kind = LiteWalletAppTransactionKind::Unknown;
std::int64_t timestamp = 0;
std::optional<std::int64_t> blockHeight;
bool unconfirmed = false;
std::string address;
std::int64_t amountZatoshis = 0;
std::int64_t signedAmountZatoshis = 0;
std::string memo;
std::optional<std::int64_t> position;
std::vector<LiteWalletAppTransactionOutputModel> outgoingOutputs;
};
struct LiteWalletAppSyncModel {
std::uint64_t walletHeight = 0;
std::uint64_t chainHeight = 0;
double progress = 0.0;
bool complete = false;
};
struct LiteWalletAppRefreshModel {
bool complete = false;
bool hasChainInfo = false;
bool hasHeight = false;
bool hasBalance = false;
bool hasAddresses = false;
bool hasSpendableOutputs = false;
bool hasTransactions = false;
bool hasSyncStatus = false;
std::size_t successfulCommandCount = 0;
LiteWalletAppChainModel chain;
LiteWalletAppHeightModel height;
LiteWalletAppBalanceModel balance;
LiteWalletAppSyncModel sync;
std::vector<LiteWalletAppAddressModel> addresses;
std::vector<LiteWalletAppSpendableOutputModel> spendableOutputs;
std::vector<LiteWalletAppTransactionModel> transactions;
};
struct LiteWalletStateMapIssueInfo {
LiteWalletStateMapIssue issue = LiteWalletStateMapIssue::EmptyBundle;
std::string message;
};
struct LiteWalletStateMapResult {
bool ok = false;
bool stateMutationAllowed = false;
LiteWalletAppRefreshModel model;
std::vector<LiteWalletStateMapIssueInfo> issues;
std::string error;
};
const char* liteWalletAppAddressKindName(LiteWalletAppAddressKind kind);
const char* liteWalletAppTransactionKindName(LiteWalletAppTransactionKind kind);
const char* liteWalletStateMapIssueName(LiteWalletStateMapIssue issue);
LiteWalletStateMapResult mapLiteWalletRefreshBundle(const LiteWalletRefreshBundle& bundle);
LiteWalletStateMapResult mapLiteWalletRefreshResult(const LiteWalletRefreshResult& result);
LiteWalletStateMapResult mapLiteWalletRefreshServiceResult(const LiteWalletRefreshServiceResult& result);
} // namespace dragonx::wallet

View File

@@ -0,0 +1,344 @@
#include "wallet/lite_wallet_sync_app_refresh_integration.h"
#include <utility>
namespace dragonx::wallet {
namespace {
void addIssue(LiteWalletSyncAppRefreshIntegrationResult& result,
LiteWalletSyncAppRefreshIntegrationIssue issue,
std::string message)
{
result.issues.push_back(LiteWalletSyncAppRefreshIntegrationIssueInfo{issue, std::move(message)});
}
std::string statusMessageOrDefault(const WalletBackendStatus& status,
const std::string& fallback)
{
return status.message.empty() ? fallback : status.message;
}
bool refreshReportIsUnsafe(const LiteWalletAppRefreshOrchestrationResult& refresh)
{
return !refresh.dryRunOnly ||
!refresh.noNetwork ||
refresh.stateMutationRequested ||
refresh.stateMutationAllowed ||
refresh.stateMutated ||
refresh.walletStateWritten;
}
bool syncStartPlanIsValid(const LiteSyncPlan& plan)
{
return plan.ok && plan.operation == LiteSyncOperation::StartSync && plan.command == "sync";
}
bool syncStatusPlanIsValid(const LiteSyncPlan& plan)
{
return plan.ok && plan.operation == LiteSyncOperation::PollSyncStatus && plan.command == "syncstatus";
}
LiteWalletSyncAppRefreshIntegrationResult stoppedResult(
LiteWalletSyncAppRefreshIntegrationResult result,
LiteWalletSyncAppRefreshIntegrationStatus status,
LiteWalletSyncAppRefreshIntegrationIssue issue,
std::string message)
{
result.status = status;
addIssue(result, issue, std::move(message));
result.error = result.issues.back().message;
return result;
}
LiteWalletSyncAppRefreshIntegrationResult skippedResult(
LiteWalletSyncAppRefreshIntegrationResult result,
LiteWalletSyncAppRefreshIntegrationIssue issue,
std::string message)
{
result.ok = true;
result.status = LiteWalletSyncAppRefreshIntegrationStatus::RefreshNotQueued;
addIssue(result, issue, std::move(message));
return result;
}
void copyRecoveryFlags(LiteWalletSyncAppRefreshIntegrationResult& result,
const LiteSyncRecoveryDecision& decision)
{
result.recoveryDecision = decision;
result.futureClearRequired = decision.shouldClear;
result.futureRescanRequired = decision.shouldRescan;
result.futureRestartSyncRequired = decision.shouldRestartSync;
result.requiresUserAttention = decision.requiresUserAttention;
}
} // namespace
const char* liteWalletSyncAppRefreshIntegrationStatusName(
LiteWalletSyncAppRefreshIntegrationStatus status)
{
switch (status) {
case LiteWalletSyncAppRefreshIntegrationStatus::FutureWorkerQueueFeedReady: return "FutureWorkerQueueFeedReady";
case LiteWalletSyncAppRefreshIntegrationStatus::WaitingForLifecycle: return "WaitingForLifecycle";
case LiteWalletSyncAppRefreshIntegrationStatus::WaitingForSyncPlan: return "WaitingForSyncPlan";
case LiteWalletSyncAppRefreshIntegrationStatus::WaitingForSyncStatus: return "WaitingForSyncStatus";
case LiteWalletSyncAppRefreshIntegrationStatus::WaitingForRecovery: return "WaitingForRecovery";
case LiteWalletSyncAppRefreshIntegrationStatus::WaitingForCancellation: return "WaitingForCancellation";
case LiteWalletSyncAppRefreshIntegrationStatus::RefreshNotQueued: return "RefreshNotQueued";
case LiteWalletSyncAppRefreshIntegrationStatus::WorkerQueueUnavailable: return "WorkerQueueUnavailable";
case LiteWalletSyncAppRefreshIntegrationStatus::UnsafePlan: return "UnsafePlan";
}
return "Unknown";
}
const char* liteWalletSyncAppRefreshIntegrationIssueName(
LiteWalletSyncAppRefreshIntegrationIssue issue)
{
switch (issue) {
case LiteWalletSyncAppRefreshIntegrationIssue::LifecycleNotReady: return "LifecycleNotReady";
case LiteWalletSyncAppRefreshIntegrationIssue::MissingSyncStartPlan: return "MissingSyncStartPlan";
case LiteWalletSyncAppRefreshIntegrationIssue::InvalidSyncStartPlan: return "InvalidSyncStartPlan";
case LiteWalletSyncAppRefreshIntegrationIssue::SyncStartWouldExecute: return "SyncStartWouldExecute";
case LiteWalletSyncAppRefreshIntegrationIssue::MissingSyncStatusPlan: return "MissingSyncStatusPlan";
case LiteWalletSyncAppRefreshIntegrationIssue::InvalidSyncStatusPlan: return "InvalidSyncStatusPlan";
case LiteWalletSyncAppRefreshIntegrationIssue::SyncStatusWouldExecute: return "SyncStatusWouldExecute";
case LiteWalletSyncAppRefreshIntegrationIssue::MissingRecoveryDecision: return "MissingRecoveryDecision";
case LiteWalletSyncAppRefreshIntegrationIssue::SyncStillPolling: return "SyncStillPolling";
case LiteWalletSyncAppRefreshIntegrationIssue::SyncRecoveryRequired: return "SyncRecoveryRequired";
case LiteWalletSyncAppRefreshIntegrationIssue::SyncRecoveryNeedsUserAttention: return "SyncRecoveryNeedsUserAttention";
case LiteWalletSyncAppRefreshIntegrationIssue::CancellationRequired: return "CancellationRequired";
case LiteWalletSyncAppRefreshIntegrationIssue::CancellationUnsupported: return "CancellationUnsupported";
case LiteWalletSyncAppRefreshIntegrationIssue::RefreshReportRejected: return "RefreshReportRejected";
case LiteWalletSyncAppRefreshIntegrationIssue::RefreshReportUnsafe: return "RefreshReportUnsafe";
case LiteWalletSyncAppRefreshIntegrationIssue::RefreshSkipped: return "RefreshSkipped";
case LiteWalletSyncAppRefreshIntegrationIssue::RefreshBlocked: return "RefreshBlocked";
case LiteWalletSyncAppRefreshIntegrationIssue::FutureWorkerQueueDisabled: return "FutureWorkerQueueDisabled";
case LiteWalletSyncAppRefreshIntegrationIssue::FutureWorkerQueuePressure: return "FutureWorkerQueuePressure";
}
return "Unknown";
}
LiteWalletSyncAppRefreshIntegrationResult planLiteWalletSyncAppRefreshIntegration(
const LiteWalletSyncAppRefreshIntegrationInput& input,
LiteWalletSyncAppRefreshIntegrationOptions options)
{
LiteWalletSyncAppRefreshIntegrationResult result;
result.lifecycleStatus = input.lifecycle.status;
result.refreshOrchestration = input.refreshOrchestration;
result.route = input.refreshOrchestration.route;
result.refreshWouldQueue = input.refreshOrchestration.wouldQueueRefresh;
result.refreshSkipped = input.refreshOrchestration.skipped;
result.refreshBlocked = input.refreshOrchestration.blocked;
result.futureWorkerQueuePlan.laneName = input.futureWorkerQueue.laneName;
result.futureWorkerQueuePlan.trigger = input.refreshOrchestration.trigger;
result.futureWorkerQueuePlan.route = input.refreshOrchestration.route;
result.futureWorkerQueuePlan.queueDepth = input.futureWorkerQueue.queueDepth;
result.futureWorkerQueuePlan.maxQueueDepth = input.futureWorkerQueue.maxQueueDepth;
if (input.sync.haveStartPlan) {
result.syncStartPlan = input.sync.startPlan;
result.futureWorkerQueuePlan.startSyncCommand = input.sync.startPlan.command;
}
if (input.sync.haveStatusPlan) {
result.syncStatusPlan = input.sync.statusPlan;
result.futureWorkerQueuePlan.syncStatusCommand = input.sync.statusPlan.command;
}
if (input.sync.haveRecoveryDecision) {
copyRecoveryFlags(result, input.sync.recoveryDecision);
}
if (options.requireLifecycleReady && !input.lifecycle.ready) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForLifecycle,
LiteWalletSyncAppRefreshIntegrationIssue::LifecycleNotReady,
statusMessageOrDefault(input.lifecycle.status, "lite wallet lifecycle is not ready"));
}
result.lifecycleAccepted = true;
if (options.requireSyncStartPlan && !input.sync.haveStartPlan) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForSyncPlan,
LiteWalletSyncAppRefreshIntegrationIssue::MissingSyncStartPlan,
"lite sync start plan is required before app refresh integration");
}
if (input.sync.haveStartPlan) {
if (!syncStartPlanIsValid(input.sync.startPlan)) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForSyncPlan,
LiteWalletSyncAppRefreshIntegrationIssue::InvalidSyncStartPlan,
input.sync.startPlan.error.empty()
? "lite sync start plan is invalid"
: input.sync.startPlan.error);
}
if (options.rejectExecutableSyncPlans && input.sync.startPlan.bridgeExecutionAllowed) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::UnsafePlan,
LiteWalletSyncAppRefreshIntegrationIssue::SyncStartWouldExecute,
"lite sync start plan would allow bridge execution");
}
result.syncStartPlanAccepted = true;
}
if (options.requireSyncStatusPlan && !input.sync.haveStatusPlan) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForSyncStatus,
LiteWalletSyncAppRefreshIntegrationIssue::MissingSyncStatusPlan,
"lite syncstatus plan is required before app refresh integration");
}
if (input.sync.haveStatusPlan) {
if (!syncStatusPlanIsValid(input.sync.statusPlan)) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForSyncStatus,
LiteWalletSyncAppRefreshIntegrationIssue::InvalidSyncStatusPlan,
input.sync.statusPlan.error.empty()
? "lite syncstatus plan is invalid"
: input.sync.statusPlan.error);
}
if (options.rejectExecutableSyncPlans && input.sync.statusPlan.bridgeExecutionAllowed) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::UnsafePlan,
LiteWalletSyncAppRefreshIntegrationIssue::SyncStatusWouldExecute,
"lite syncstatus plan would allow bridge execution");
}
result.syncStatusPlanAccepted = true;
}
if (options.requireRecoveryDecision && !input.sync.haveRecoveryDecision) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForSyncStatus,
LiteWalletSyncAppRefreshIntegrationIssue::MissingRecoveryDecision,
"lite sync recovery decision is required before app refresh integration");
}
if (input.sync.haveRecoveryDecision) {
const auto& decision = input.sync.recoveryDecision;
if (decision.kind == LiteSyncRecoveryDecisionKind::InvalidStatus) {
result.recoveryRequired = true;
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForRecovery,
LiteWalletSyncAppRefreshIntegrationIssue::SyncRecoveryNeedsUserAttention,
decision.reason.empty()
? "lite sync status is invalid and needs user attention"
: decision.reason);
}
if (decision.kind == LiteSyncRecoveryDecisionKind::Stuck ||
decision.kind == LiteSyncRecoveryDecisionKind::ReorgDetected) {
result.recoveryRequired = true;
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForRecovery,
LiteWalletSyncAppRefreshIntegrationIssue::SyncRecoveryRequired,
decision.reason.empty()
? "lite sync recovery must be modeled before refresh can feed a queue"
: decision.reason);
}
if (options.requireSyncCompleteForRefresh && decision.kind == LiteSyncRecoveryDecisionKind::KeepPolling) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForSyncStatus,
LiteWalletSyncAppRefreshIntegrationIssue::SyncStillPolling,
decision.reason.empty()
? "lite sync is still polling"
: decision.reason);
}
result.recoveryAccepted = true;
}
if (input.cancellation.cancellationRequested && input.cancellation.syncInProgress) {
result.cancellationRequired = true;
if (!input.cancellation.cancellationSupported) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForCancellation,
LiteWalletSyncAppRefreshIntegrationIssue::CancellationUnsupported,
"lite sync cancellation is requested but no cancellation path is available");
}
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WaitingForCancellation,
LiteWalletSyncAppRefreshIntegrationIssue::CancellationRequired,
"lite sync cancellation must complete before app refresh can feed a queue");
}
result.cancellationAccepted = true;
if (refreshReportIsUnsafe(input.refreshOrchestration)) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::UnsafePlan,
LiteWalletSyncAppRefreshIntegrationIssue::RefreshReportUnsafe,
"lite app refresh orchestration report is not a no-network dry-run report");
}
if (!input.refreshOrchestration.ok) {
const auto issue = input.refreshOrchestration.blocked
? LiteWalletSyncAppRefreshIntegrationIssue::RefreshBlocked
: LiteWalletSyncAppRefreshIntegrationIssue::RefreshReportRejected;
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::RefreshNotQueued,
issue,
input.refreshOrchestration.error.empty()
? "lite app refresh orchestration report is not queued"
: input.refreshOrchestration.error);
}
result.refreshReportAccepted = true;
if (input.refreshOrchestration.skipped || !input.refreshOrchestration.wouldQueueRefresh) {
return skippedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationIssue::RefreshSkipped,
input.refreshOrchestration.issues.empty()
? "lite app refresh orchestration skipped queueing"
: input.refreshOrchestration.issues.front().message);
}
if (options.requireFutureWorkerQueue && !input.futureWorkerQueue.enabled) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WorkerQueueUnavailable,
LiteWalletSyncAppRefreshIntegrationIssue::FutureWorkerQueueDisabled,
"future lite app refresh worker queue is disabled");
}
if (input.futureWorkerQueue.maxQueueDepth > 0 &&
input.futureWorkerQueue.queueDepth >= input.futureWorkerQueue.maxQueueDepth) {
return stoppedResult(
std::move(result),
LiteWalletSyncAppRefreshIntegrationStatus::WorkerQueueUnavailable,
LiteWalletSyncAppRefreshIntegrationIssue::FutureWorkerQueuePressure,
"future lite app refresh worker queue is at capacity");
}
result.ok = true;
result.status = LiteWalletSyncAppRefreshIntegrationStatus::FutureWorkerQueueFeedReady;
result.futureWorkerQueueFeedReady = true;
result.wouldFeedFutureWorkerQueue = true;
result.futureWorkerQueuePlan.readyToFeed = true;
result.futureWorkerQueuePlan.wouldFeed = true;
return result;
}
LiteWalletSyncAppRefreshIntegrationPlanner::LiteWalletSyncAppRefreshIntegrationPlanner(
LiteWalletSyncAppRefreshIntegrationOptions options)
: options_(options)
{
}
LiteWalletSyncAppRefreshIntegrationResult LiteWalletSyncAppRefreshIntegrationPlanner::plan(
const LiteWalletSyncAppRefreshIntegrationInput& input) const
{
return planLiteWalletSyncAppRefreshIntegration(input, options_);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,172 @@
#pragma once
#include "lite_sync_service.h"
#include "lite_wallet_app_refresh_orchestrator.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletSyncAppRefreshIntegrationStatus {
FutureWorkerQueueFeedReady,
WaitingForLifecycle,
WaitingForSyncPlan,
WaitingForSyncStatus,
WaitingForRecovery,
WaitingForCancellation,
RefreshNotQueued,
WorkerQueueUnavailable,
UnsafePlan,
};
enum class LiteWalletSyncAppRefreshIntegrationIssue {
LifecycleNotReady,
MissingSyncStartPlan,
InvalidSyncStartPlan,
SyncStartWouldExecute,
MissingSyncStatusPlan,
InvalidSyncStatusPlan,
SyncStatusWouldExecute,
MissingRecoveryDecision,
SyncStillPolling,
SyncRecoveryRequired,
SyncRecoveryNeedsUserAttention,
CancellationRequired,
CancellationUnsupported,
RefreshReportRejected,
RefreshReportUnsafe,
RefreshSkipped,
RefreshBlocked,
FutureWorkerQueueDisabled,
FutureWorkerQueuePressure,
};
struct LiteWalletSyncAppRefreshLifecycleInput {
bool ready = false;
WalletBackendStatus status;
};
struct LiteWalletSyncAppRefreshSyncPlanInput {
bool haveStartPlan = false;
LiteSyncPlan startPlan;
bool haveStatusPlan = false;
LiteSyncPlan statusPlan;
bool haveRecoveryDecision = false;
LiteSyncRecoveryDecision recoveryDecision;
};
struct LiteWalletSyncAppRefreshCancellationInput {
bool cancellationRequested = false;
bool syncInProgress = false;
bool cancellationSupported = false;
};
struct LiteWalletFutureWorkerQueueInput {
bool enabled = true;
std::size_t queueDepth = 0;
std::size_t maxQueueDepth = 0;
std::string laneName = "lite-app-refresh";
};
struct LiteWalletSyncAppRefreshIntegrationInput {
LiteWalletSyncAppRefreshLifecycleInput lifecycle;
LiteWalletSyncAppRefreshSyncPlanInput sync;
LiteWalletSyncAppRefreshCancellationInput cancellation;
LiteWalletFutureWorkerQueueInput futureWorkerQueue;
LiteWalletAppRefreshOrchestrationResult refreshOrchestration;
};
struct LiteWalletSyncAppRefreshIntegrationOptions {
bool requireLifecycleReady = true;
bool requireSyncStartPlan = true;
bool requireSyncStatusPlan = true;
bool requireRecoveryDecision = true;
bool requireSyncCompleteForRefresh = true;
bool rejectExecutableSyncPlans = true;
bool requireFutureWorkerQueue = true;
};
struct LiteWalletFutureWorkerQueuePlan {
bool readyToFeed = false;
bool wouldFeed = false;
bool enqueued = false;
std::string laneName;
LiteWalletAppRefreshScheduleTrigger trigger = LiteWalletAppRefreshScheduleTrigger::Periodic;
LiteWalletRefreshRouteKind route = LiteWalletRefreshRouteKind::Unavailable;
std::string startSyncCommand;
std::string syncStatusCommand;
std::size_t queueDepth = 0;
std::size_t maxQueueDepth = 0;
};
struct LiteWalletSyncAppRefreshIntegrationIssueInfo {
LiteWalletSyncAppRefreshIntegrationIssue issue = LiteWalletSyncAppRefreshIntegrationIssue::LifecycleNotReady;
std::string message;
};
struct LiteWalletSyncAppRefreshIntegrationResult {
bool ok = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool noBridgeCalls = true;
bool noSyncStarted = true;
bool noWalletPersistence = true;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool workerQueueEnqueued = false;
bool lifecycleAccepted = false;
bool syncStartPlanAccepted = false;
bool syncStatusPlanAccepted = false;
bool recoveryAccepted = false;
bool cancellationAccepted = false;
bool refreshReportAccepted = false;
bool futureWorkerQueueFeedReady = false;
bool wouldFeedFutureWorkerQueue = false;
bool refreshWouldQueue = false;
bool refreshSkipped = false;
bool refreshBlocked = false;
bool recoveryRequired = false;
bool cancellationRequired = false;
bool futureClearRequired = false;
bool futureRescanRequired = false;
bool futureRestartSyncRequired = false;
bool requiresUserAttention = false;
LiteWalletSyncAppRefreshIntegrationStatus status = LiteWalletSyncAppRefreshIntegrationStatus::WaitingForLifecycle;
LiteWalletRefreshRouteKind route = LiteWalletRefreshRouteKind::Unavailable;
WalletBackendStatus lifecycleStatus;
LiteSyncPlan syncStartPlan;
LiteSyncPlan syncStatusPlan;
LiteSyncRecoveryDecision recoveryDecision;
LiteWalletAppRefreshOrchestrationResult refreshOrchestration;
LiteWalletFutureWorkerQueuePlan futureWorkerQueuePlan;
std::vector<LiteWalletSyncAppRefreshIntegrationIssueInfo> issues;
std::string error;
};
const char* liteWalletSyncAppRefreshIntegrationStatusName(
LiteWalletSyncAppRefreshIntegrationStatus status);
const char* liteWalletSyncAppRefreshIntegrationIssueName(
LiteWalletSyncAppRefreshIntegrationIssue issue);
LiteWalletSyncAppRefreshIntegrationResult planLiteWalletSyncAppRefreshIntegration(
const LiteWalletSyncAppRefreshIntegrationInput& input,
LiteWalletSyncAppRefreshIntegrationOptions options = {});
class LiteWalletSyncAppRefreshIntegrationPlanner {
public:
explicit LiteWalletSyncAppRefreshIntegrationPlanner(
LiteWalletSyncAppRefreshIntegrationOptions options = {});
LiteWalletSyncAppRefreshIntegrationResult plan(
const LiteWalletSyncAppRefreshIntegrationInput& input) const;
private:
LiteWalletSyncAppRefreshIntegrationOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,461 @@
#include "wallet/lite_wallet_sync_execution_readiness.h"
#include <utility>
namespace dragonx::wallet {
namespace {
void addIssue(LiteWalletSyncExecutionReadinessResult& result,
LiteWalletSyncExecutionReadinessIssue issue,
std::string message)
{
result.issues.push_back(LiteWalletSyncExecutionReadinessIssueInfo{issue, std::move(message)});
}
LiteWalletSyncExecutionReadinessResult stoppedResult(
LiteWalletSyncExecutionReadinessResult result,
LiteWalletSyncExecutionReadinessStatus status,
LiteWalletSyncExecutionReadinessIssue issue,
std::string message)
{
result.status = status;
addIssue(result, issue, std::move(message));
result.error = result.issues.back().message;
return result;
}
bool coreArtifactSymbolsReady(const LiteWalletSdxlArtifactSymbolsInput& symbols)
{
return symbols.walletExists &&
symbols.initializeNew &&
symbols.initializeNewFromPhrase &&
symbols.initializeExisting &&
symbols.execute &&
symbols.checkServerOnline;
}
bool integrationReportIsUnsafe(const LiteWalletSyncAppRefreshIntegrationResult& integration)
{
return !integration.dryRunOnly ||
!integration.noNetwork ||
!integration.noBridgeCalls ||
!integration.noSyncStarted ||
!integration.noWalletPersistence ||
integration.stateMutationAllowed ||
integration.stateMutated ||
integration.walletStateWritten ||
integration.workerQueueEnqueued;
}
std::string backendMessageOrDefault(const WalletBackendStatus& status,
const std::string& fallback)
{
return status.message.empty() ? fallback : status.message;
}
bool recoveryRequiredByIntegration(const LiteWalletSyncAppRefreshIntegrationResult& integration)
{
return integration.recoveryRequired ||
integration.futureClearRequired ||
integration.futureRescanRequired ||
integration.futureRestartSyncRequired ||
integration.requiresUserAttention;
}
bool cancellationRequiredByIntegration(const LiteWalletSyncAppRefreshIntegrationResult& integration)
{
return integration.cancellationRequired ||
integration.status == LiteWalletSyncAppRefreshIntegrationStatus::WaitingForCancellation;
}
void copyIntegrationSummary(LiteWalletSyncExecutionReadinessResult& result,
const LiteWalletSyncAppRefreshIntegrationResult& integration)
{
result.integrationReport = integration;
result.integrationFeedReady = integration.futureWorkerQueueFeedReady;
result.recoveryRequired = recoveryRequiredByIntegration(integration);
result.cancellationRequired = cancellationRequiredByIntegration(integration);
result.futureClearRequired = integration.futureClearRequired;
result.futureRescanRequired = integration.futureRescanRequired;
result.futureRestartSyncRequired = integration.futureRestartSyncRequired;
result.requiresUserAttention = integration.requiresUserAttention;
result.enablementPlan.startSyncCommand = integration.syncStartPlan.command;
result.enablementPlan.syncStatusCommand = integration.syncStatusPlan.command;
result.enablementPlan.workerQueueLane = integration.futureWorkerQueuePlan.laneName;
result.enablementPlan.route = integration.route;
}
LiteWalletSyncExecutionReadinessResult evaluateRecoveryReadiness(
LiteWalletSyncExecutionReadinessResult result,
const LiteWalletSyncExecutionRecoveryReadinessInput& recovery,
LiteWalletSyncExecutionReadinessOptions options)
{
if (!options.requireRecoveryExecutionReadiness) {
result.recoveryExecutionReady = true;
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForRecovery,
LiteWalletSyncExecutionReadinessIssue::IntegrationFeedNotReady,
"lite sync recovery must complete before sync execution can be enabled");
}
if (result.futureClearRequired && !recovery.clearActionReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForRecovery,
LiteWalletSyncExecutionReadinessIssue::RecoveryClearMissing,
"lite sync recovery requires a future clear action before sync execution");
}
if (result.futureRescanRequired && !recovery.rescanActionReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForRecovery,
LiteWalletSyncExecutionReadinessIssue::RecoveryRescanMissing,
"lite sync recovery requires a future rescan action before sync execution");
}
if (result.futureRestartSyncRequired && !recovery.restartSyncActionReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForRecovery,
LiteWalletSyncExecutionReadinessIssue::RecoveryRestartMissing,
"lite sync recovery requires a future restart-sync action before sync execution");
}
if (result.requiresUserAttention && !recovery.userAttentionPathReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForRecovery,
LiteWalletSyncExecutionReadinessIssue::RecoveryUserAttentionMissing,
"lite sync recovery requires a user-attention path before sync execution");
}
result.recoveryExecutionReady = true;
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForRecovery,
LiteWalletSyncExecutionReadinessIssue::IntegrationFeedNotReady,
"lite sync recovery is ready to be modeled but must complete before sync execution");
}
} // namespace
const char* liteWalletSyncExecutionReadinessStatusName(
LiteWalletSyncExecutionReadinessStatus status)
{
switch (status) {
case LiteWalletSyncExecutionReadinessStatus::ReadyToEnableSyncExecution: return "ReadyToEnableSyncExecution";
case LiteWalletSyncExecutionReadinessStatus::WaitingForLiteBuild: return "WaitingForLiteBuild";
case LiteWalletSyncExecutionReadinessStatus::WaitingForArtifact: return "WaitingForArtifact";
case LiteWalletSyncExecutionReadinessStatus::WaitingForBackendLink: return "WaitingForBackendLink";
case LiteWalletSyncExecutionReadinessStatus::WaitingForBridge: return "WaitingForBridge";
case LiteWalletSyncExecutionReadinessStatus::WaitingForIntegration: return "WaitingForIntegration";
case LiteWalletSyncExecutionReadinessStatus::WaitingForExecutionOwner: return "WaitingForExecutionOwner";
case LiteWalletSyncExecutionReadinessStatus::WaitingForCancellation: return "WaitingForCancellation";
case LiteWalletSyncExecutionReadinessStatus::WaitingForShutdown: return "WaitingForShutdown";
case LiteWalletSyncExecutionReadinessStatus::WaitingForRetry: return "WaitingForRetry";
case LiteWalletSyncExecutionReadinessStatus::WaitingForRecovery: return "WaitingForRecovery";
case LiteWalletSyncExecutionReadinessStatus::UnsafePlan: return "UnsafePlan";
}
return "Unknown";
}
const char* liteWalletSyncExecutionReadinessIssueName(
LiteWalletSyncExecutionReadinessIssue issue)
{
switch (issue) {
case LiteWalletSyncExecutionReadinessIssue::FullNodeBuild: return "FullNodeBuild";
case LiteWalletSyncExecutionReadinessIssue::LiteBackendCapabilityMissing: return "LiteBackendCapabilityMissing";
case LiteWalletSyncExecutionReadinessIssue::ArtifactPathMissing: return "ArtifactPathMissing";
case LiteWalletSyncExecutionReadinessIssue::ArtifactMissing: return "ArtifactMissing";
case LiteWalletSyncExecutionReadinessIssue::ArtifactUnreadable: return "ArtifactUnreadable";
case LiteWalletSyncExecutionReadinessIssue::ArtifactNotSdxlCompatible: return "ArtifactNotSdxlCompatible";
case LiteWalletSyncExecutionReadinessIssue::ArtifactSymbolsMissing: return "ArtifactSymbolsMissing";
case LiteWalletSyncExecutionReadinessIssue::ArtifactStringOwnershipUnverified: return "ArtifactStringOwnershipUnverified";
case LiteWalletSyncExecutionReadinessIssue::ArtifactShutdownUnavailable: return "ArtifactShutdownUnavailable";
case LiteWalletSyncExecutionReadinessIssue::BackendNotLinked: return "BackendNotLinked";
case LiteWalletSyncExecutionReadinessIssue::BridgeUnavailable: return "BridgeUnavailable";
case LiteWalletSyncExecutionReadinessIssue::IntegrationRejected: return "IntegrationRejected";
case LiteWalletSyncExecutionReadinessIssue::IntegrationUnsafe: return "IntegrationUnsafe";
case LiteWalletSyncExecutionReadinessIssue::IntegrationFeedNotReady: return "IntegrationFeedNotReady";
case LiteWalletSyncExecutionReadinessIssue::WorkerQueueOwnerMissing: return "WorkerQueueOwnerMissing";
case LiteWalletSyncExecutionReadinessIssue::SyncLoopOwnerMissing: return "SyncLoopOwnerMissing";
case LiteWalletSyncExecutionReadinessIssue::StartSyncExecutionMissing: return "StartSyncExecutionMissing";
case LiteWalletSyncExecutionReadinessIssue::SyncStatusExecutionMissing: return "SyncStatusExecutionMissing";
case LiteWalletSyncExecutionReadinessIssue::CancellationPathMissing: return "CancellationPathMissing";
case LiteWalletSyncExecutionReadinessIssue::CancellationPending: return "CancellationPending";
case LiteWalletSyncExecutionReadinessIssue::ShutdownPathMissing: return "ShutdownPathMissing";
case LiteWalletSyncExecutionReadinessIssue::ShutdownPending: return "ShutdownPending";
case LiteWalletSyncExecutionReadinessIssue::RetryPolicyMissing: return "RetryPolicyMissing";
case LiteWalletSyncExecutionReadinessIssue::RetryLimitReached: return "RetryLimitReached";
case LiteWalletSyncExecutionReadinessIssue::UserVisibleStatusMissing: return "UserVisibleStatusMissing";
case LiteWalletSyncExecutionReadinessIssue::RecoveryClearMissing: return "RecoveryClearMissing";
case LiteWalletSyncExecutionReadinessIssue::RecoveryRescanMissing: return "RecoveryRescanMissing";
case LiteWalletSyncExecutionReadinessIssue::RecoveryRestartMissing: return "RecoveryRestartMissing";
case LiteWalletSyncExecutionReadinessIssue::RecoveryUserAttentionMissing: return "RecoveryUserAttentionMissing";
}
return "Unknown";
}
LiteWalletSyncExecutionReadinessResult evaluateLiteWalletSyncExecutionReadiness(
const LiteWalletSyncExecutionReadinessInput& input,
LiteWalletSyncExecutionReadinessOptions options)
{
LiteWalletSyncExecutionReadinessResult result;
result.capabilities = input.capabilities;
result.backendStatus = input.backend.status;
copyIntegrationSummary(result, input.integrationReport);
if (options.requireLiteBuild && !isLiteBuild(input.capabilities)) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForLiteBuild,
LiteWalletSyncExecutionReadinessIssue::FullNodeBuild,
"lite sync execution readiness requires a lite build");
}
result.liteBuildAccepted = true;
if (options.requireLiteBackendCapability && !supportsLiteBackend(input.capabilities)) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForBackendLink,
LiteWalletSyncExecutionReadinessIssue::LiteBackendCapabilityMissing,
"lite backend capability is not available");
}
if (options.requireValidatedArtifact) {
if (!input.artifact.pathConfigured || input.artifact.artifactPath.empty()) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForArtifact,
LiteWalletSyncExecutionReadinessIssue::ArtifactPathMissing,
"SDXL-compatible lite backend artifact path is not configured");
}
if (!input.artifact.exists) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForArtifact,
LiteWalletSyncExecutionReadinessIssue::ArtifactMissing,
"SDXL-compatible lite backend artifact does not exist");
}
if (!input.artifact.readable) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForArtifact,
LiteWalletSyncExecutionReadinessIssue::ArtifactUnreadable,
"SDXL-compatible lite backend artifact is not readable");
}
if (!input.artifact.sdxlCompatible) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForArtifact,
LiteWalletSyncExecutionReadinessIssue::ArtifactNotSdxlCompatible,
"lite backend artifact has not been validated as SDXL-compatible");
}
if (!coreArtifactSymbolsReady(input.artifact.symbols)) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForArtifact,
LiteWalletSyncExecutionReadinessIssue::ArtifactSymbolsMissing,
"lite backend artifact is missing required SDXL lifecycle or execute symbols");
}
if (!input.artifact.symbols.freeString) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForArtifact,
LiteWalletSyncExecutionReadinessIssue::ArtifactStringOwnershipUnverified,
"lite backend artifact string ownership cleanup is not validated");
}
if (!input.artifact.symbols.shutdown) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForArtifact,
LiteWalletSyncExecutionReadinessIssue::ArtifactShutdownUnavailable,
"lite backend artifact shutdown symbol is not validated");
}
}
result.artifactAccepted = true;
if (options.requireLinkedBackend && !input.backend.linked) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForBackendLink,
LiteWalletSyncExecutionReadinessIssue::BackendNotLinked,
backendMessageOrDefault(input.backend.status, "lite backend is not linked"));
}
result.backendLinkedAccepted = true;
if (options.requireBridgeAvailable && !input.backend.bridgeAvailable) {
const auto message = !input.backend.bridgeUnavailableReason.empty()
? input.backend.bridgeUnavailableReason
: backendMessageOrDefault(input.backend.status, "lite client bridge is unavailable");
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForBridge,
LiteWalletSyncExecutionReadinessIssue::BridgeUnavailable,
message);
}
result.bridgeAccepted = true;
if (integrationReportIsUnsafe(input.integrationReport)) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::UnsafePlan,
LiteWalletSyncExecutionReadinessIssue::IntegrationUnsafe,
"lite sync/app-refresh integration report is not a no-network dry-run report");
}
if (result.recoveryRequired) {
return evaluateRecoveryReadiness(std::move(result), input.recovery, options);
}
if (result.cancellationRequired || input.cancellation.cancellationRequested) {
result.cancellationRequired = true;
if (!input.cancellation.cancellationPathReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForCancellation,
LiteWalletSyncExecutionReadinessIssue::CancellationPathMissing,
"lite sync cancellation path must be ready before sync execution");
}
if (!input.cancellation.cancellationComplete) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForCancellation,
LiteWalletSyncExecutionReadinessIssue::CancellationPending,
"lite sync cancellation must complete before sync execution");
}
}
if (!input.integrationReport.ok) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForIntegration,
LiteWalletSyncExecutionReadinessIssue::IntegrationRejected,
input.integrationReport.error.empty()
? "lite sync/app-refresh integration report is not successful"
: input.integrationReport.error);
}
if (options.requireIntegrationFeedReady && !input.integrationReport.futureWorkerQueueFeedReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForIntegration,
LiteWalletSyncExecutionReadinessIssue::IntegrationFeedNotReady,
"lite sync/app-refresh integration report is not ready to feed future work");
}
result.integrationAccepted = true;
if (options.requireWorkerQueueOwner && !input.ownership.workerQueueOwnerReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForExecutionOwner,
LiteWalletSyncExecutionReadinessIssue::WorkerQueueOwnerMissing,
"lite sync execution requires explicit worker queue ownership");
}
result.workerQueueOwnerAccepted = true;
if (options.requireSyncLoopOwner && !input.ownership.syncLoopOwnerReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForExecutionOwner,
LiteWalletSyncExecutionReadinessIssue::SyncLoopOwnerMissing,
"lite sync execution requires explicit sync loop ownership");
}
result.syncLoopOwnerAccepted = true;
if (options.requireStartSyncExecution && !input.ownership.startSyncExecutionReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForExecutionOwner,
LiteWalletSyncExecutionReadinessIssue::StartSyncExecutionMissing,
"lite sync start execution path is not ready");
}
result.startSyncExecutionReady = true;
if (options.requireSyncStatusPolling && !input.ownership.syncStatusPollingReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForExecutionOwner,
LiteWalletSyncExecutionReadinessIssue::SyncStatusExecutionMissing,
"lite syncstatus polling execution path is not ready");
}
result.syncStatusPollingReady = true;
if (options.requireCancellationPath && !input.cancellation.cancellationPathReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForCancellation,
LiteWalletSyncExecutionReadinessIssue::CancellationPathMissing,
"lite sync cancellation path must be ready before sync execution");
}
result.cancellationReady = true;
if (options.requireShutdownHook && !input.shutdown.shutdownHookReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForShutdown,
LiteWalletSyncExecutionReadinessIssue::ShutdownPathMissing,
"lite sync shutdown hook must be ready before sync execution");
}
if (input.shutdown.shutdownRequested && !input.shutdown.shutdownComplete) {
result.shutdownRequired = true;
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForShutdown,
LiteWalletSyncExecutionReadinessIssue::ShutdownPending,
"lite sync shutdown must complete before new sync execution");
}
result.shutdownReady = true;
if (options.requireRetryPolicy && !input.retry.retryPolicyReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForRetry,
LiteWalletSyncExecutionReadinessIssue::RetryPolicyMissing,
"lite sync retry policy must be ready before sync execution");
}
result.retryReady = true;
if (options.requireUserVisibleStatus && !input.ownership.userVisibleStatusReady) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForRetry,
LiteWalletSyncExecutionReadinessIssue::UserVisibleStatusMissing,
"lite sync execution requires user-visible status reporting");
}
result.userVisibleStatusReady = true;
if (input.retry.retryRequested) {
result.retryRequired = true;
if (input.retry.maxAttempts > 0 && input.retry.attempt >= input.retry.maxAttempts) {
return stoppedResult(
std::move(result),
LiteWalletSyncExecutionReadinessStatus::WaitingForRetry,
LiteWalletSyncExecutionReadinessIssue::RetryLimitReached,
"lite sync retry limit has been reached");
}
}
result.ok = true;
result.status = LiteWalletSyncExecutionReadinessStatus::ReadyToEnableSyncExecution;
result.syncExecutionCouldStart = true;
result.syncStatusPollingCouldStart = true;
result.executionWouldUseBridge = true;
result.enablementPlan.readyToEnable = true;
result.enablementPlan.startSyncExecutionEnabled = true;
result.enablementPlan.syncStatusPollingEnabled = true;
return result;
}
LiteWalletSyncExecutionReadinessPlanner::LiteWalletSyncExecutionReadinessPlanner(
LiteWalletSyncExecutionReadinessOptions options)
: options_(options)
{
}
LiteWalletSyncExecutionReadinessResult LiteWalletSyncExecutionReadinessPlanner::evaluate(
const LiteWalletSyncExecutionReadinessInput& input) const
{
return evaluateLiteWalletSyncExecutionReadiness(input, options_);
}
} // namespace dragonx::wallet

View File

@@ -0,0 +1,238 @@
#pragma once
#include "lite_wallet_sync_app_refresh_integration.h"
#include "wallet_capabilities.h"
#include <cstddef>
#include <string>
#include <vector>
namespace dragonx::wallet {
enum class LiteWalletSyncExecutionReadinessStatus {
ReadyToEnableSyncExecution,
WaitingForLiteBuild,
WaitingForArtifact,
WaitingForBackendLink,
WaitingForBridge,
WaitingForIntegration,
WaitingForExecutionOwner,
WaitingForCancellation,
WaitingForShutdown,
WaitingForRetry,
WaitingForRecovery,
UnsafePlan,
};
enum class LiteWalletSyncExecutionReadinessIssue {
FullNodeBuild,
LiteBackendCapabilityMissing,
ArtifactPathMissing,
ArtifactMissing,
ArtifactUnreadable,
ArtifactNotSdxlCompatible,
ArtifactSymbolsMissing,
ArtifactStringOwnershipUnverified,
ArtifactShutdownUnavailable,
BackendNotLinked,
BridgeUnavailable,
IntegrationRejected,
IntegrationUnsafe,
IntegrationFeedNotReady,
WorkerQueueOwnerMissing,
SyncLoopOwnerMissing,
StartSyncExecutionMissing,
SyncStatusExecutionMissing,
CancellationPathMissing,
CancellationPending,
ShutdownPathMissing,
ShutdownPending,
RetryPolicyMissing,
RetryLimitReached,
UserVisibleStatusMissing,
RecoveryClearMissing,
RecoveryRescanMissing,
RecoveryRestartMissing,
RecoveryUserAttentionMissing,
};
struct LiteWalletSdxlArtifactSymbolsInput {
bool walletExists = false;
bool initializeNew = false;
bool initializeNewFromPhrase = false;
bool initializeExisting = false;
bool execute = false;
bool freeString = false;
bool checkServerOnline = false;
bool shutdown = false;
};
struct LiteWalletSdxlArtifactInput {
bool pathConfigured = false;
bool exists = false;
bool readable = false;
bool sdxlCompatible = false;
std::string artifactPath;
std::string versionLabel;
LiteWalletSdxlArtifactSymbolsInput symbols;
};
struct LiteWalletLinkedBackendReadinessInput {
bool linked = false;
bool bridgeAvailable = false;
WalletBackendStatus status;
std::string bridgeUnavailableReason;
};
struct LiteWalletSyncExecutionOwnershipInput {
bool workerQueueOwnerReady = false;
bool syncLoopOwnerReady = false;
bool startSyncExecutionReady = false;
bool syncStatusPollingReady = false;
bool userVisibleStatusReady = false;
};
struct LiteWalletSyncExecutionCancellationReadinessInput {
bool cancellationPathReady = false;
bool cancellationRequested = false;
bool cancellationComplete = false;
};
struct LiteWalletSyncExecutionShutdownReadinessInput {
bool shutdownHookReady = false;
bool shutdownRequested = false;
bool shutdownComplete = false;
};
struct LiteWalletSyncExecutionRetryReadinessInput {
bool retryPolicyReady = false;
bool retryRequested = false;
std::size_t attempt = 0;
std::size_t maxAttempts = 0;
};
struct LiteWalletSyncExecutionRecoveryReadinessInput {
bool clearActionReady = false;
bool rescanActionReady = false;
bool restartSyncActionReady = false;
bool userAttentionPathReady = false;
};
struct LiteWalletSyncExecutionReadinessInput {
WalletCapabilities capabilities;
LiteWalletSdxlArtifactInput artifact;
LiteWalletLinkedBackendReadinessInput backend;
LiteWalletSyncExecutionOwnershipInput ownership;
LiteWalletSyncExecutionCancellationReadinessInput cancellation;
LiteWalletSyncExecutionShutdownReadinessInput shutdown;
LiteWalletSyncExecutionRetryReadinessInput retry;
LiteWalletSyncExecutionRecoveryReadinessInput recovery;
LiteWalletSyncAppRefreshIntegrationResult integrationReport;
};
struct LiteWalletSyncExecutionReadinessOptions {
bool requireLiteBuild = true;
bool requireLiteBackendCapability = true;
bool requireValidatedArtifact = true;
bool requireLinkedBackend = true;
bool requireBridgeAvailable = true;
bool requireIntegrationFeedReady = true;
bool requireWorkerQueueOwner = true;
bool requireSyncLoopOwner = true;
bool requireStartSyncExecution = true;
bool requireSyncStatusPolling = true;
bool requireCancellationPath = true;
bool requireShutdownHook = true;
bool requireRetryPolicy = true;
bool requireUserVisibleStatus = true;
bool requireRecoveryExecutionReadiness = true;
};
struct LiteWalletSyncExecutionEnablementPlan {
bool readyToEnable = false;
bool startSyncExecutionEnabled = false;
bool syncStatusPollingEnabled = false;
bool workerQueueEnqueued = false;
std::string startSyncCommand;
std::string syncStatusCommand;
std::string workerQueueLane;
LiteWalletRefreshRouteKind route = LiteWalletRefreshRouteKind::Unavailable;
};
struct LiteWalletSyncExecutionReadinessIssueInfo {
LiteWalletSyncExecutionReadinessIssue issue = LiteWalletSyncExecutionReadinessIssue::FullNodeBuild;
std::string message;
};
struct LiteWalletSyncExecutionReadinessResult {
bool ok = false;
bool dryRunOnly = true;
bool noNetwork = true;
bool noBridgeCalls = true;
bool noSyncStarted = true;
bool noSyncStatusPolled = true;
bool noWalletPersistence = true;
bool stateMutationAllowed = false;
bool stateMutated = false;
bool walletStateWritten = false;
bool workerQueueEnqueued = false;
bool liteBuildAccepted = false;
bool artifactAccepted = false;
bool backendLinkedAccepted = false;
bool bridgeAccepted = false;
bool integrationAccepted = false;
bool workerQueueOwnerAccepted = false;
bool syncLoopOwnerAccepted = false;
bool startSyncExecutionReady = false;
bool syncStatusPollingReady = false;
bool cancellationReady = false;
bool shutdownReady = false;
bool retryReady = false;
bool userVisibleStatusReady = false;
bool recoveryExecutionReady = false;
bool syncExecutionCouldStart = false;
bool syncStatusPollingCouldStart = false;
bool executionWouldUseBridge = false;
bool integrationFeedReady = false;
bool recoveryRequired = false;
bool cancellationRequired = false;
bool shutdownRequired = false;
bool retryRequired = false;
bool futureClearRequired = false;
bool futureRescanRequired = false;
bool futureRestartSyncRequired = false;
bool requiresUserAttention = false;
LiteWalletSyncExecutionReadinessStatus status = LiteWalletSyncExecutionReadinessStatus::WaitingForLiteBuild;
WalletCapabilities capabilities;
WalletBackendStatus backendStatus;
LiteWalletSyncAppRefreshIntegrationResult integrationReport;
LiteWalletSyncExecutionEnablementPlan enablementPlan;
std::vector<LiteWalletSyncExecutionReadinessIssueInfo> issues;
std::string error;
};
const char* liteWalletSyncExecutionReadinessStatusName(
LiteWalletSyncExecutionReadinessStatus status);
const char* liteWalletSyncExecutionReadinessIssueName(
LiteWalletSyncExecutionReadinessIssue issue);
LiteWalletSyncExecutionReadinessResult evaluateLiteWalletSyncExecutionReadiness(
const LiteWalletSyncExecutionReadinessInput& input,
LiteWalletSyncExecutionReadinessOptions options = {});
class LiteWalletSyncExecutionReadinessPlanner {
public:
explicit LiteWalletSyncExecutionReadinessPlanner(
LiteWalletSyncExecutionReadinessOptions options = {});
LiteWalletSyncExecutionReadinessResult evaluate(
const LiteWalletSyncExecutionReadinessInput& input) const;
private:
LiteWalletSyncExecutionReadinessOptions options_;
};
} // namespace dragonx::wallet

View File

@@ -0,0 +1,93 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "wallet_capabilities.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace dragonx {
namespace wallet {
enum class WalletBackendState {
Unavailable,
Disconnected,
Connecting,
Syncing,
Ready,
Error
};
struct WalletBackendStatus {
WalletBackendState state = WalletBackendState::Unavailable;
std::string message;
std::optional<int> walletHeight;
std::optional<int> chainHeight;
double syncProgress = 0.0;
};
struct WalletBackendResult {
bool ok = false;
std::string error;
};
struct WalletOpenRequest {
std::string walletPath;
std::string serverUrl;
bool restoreFromSeed = false;
};
struct WalletSendRequest {
std::string fromAddress;
std::string toAddress;
double amount = 0.0;
double fee = 0.0;
std::string memo;
};
struct WalletSendResult {
bool accepted = false;
std::string txid;
std::string error;
};
class WalletBackend {
public:
virtual ~WalletBackend() = default;
virtual WalletBackendKind kind() const = 0;
virtual WalletCapabilities capabilities() const = 0;
virtual WalletBackendStatus status() const = 0;
};
class FullNodeWalletBackend : public WalletBackend {
public:
~FullNodeWalletBackend() override = default;
};
class LiteWalletBackend : public WalletBackend {
public:
~LiteWalletBackend() override = default;
virtual WalletBackendResult setServer(const std::string& serverUrl) = 0;
virtual WalletBackendResult openWallet(const WalletOpenRequest& request) = 0;
virtual WalletBackendResult startSync() = 0;
virtual WalletBackendResult cancelSync() = 0;
virtual WalletSendResult sendTransaction(const WalletSendRequest& request) = 0;
};
class WalletBackendRouter {
public:
virtual ~WalletBackendRouter() = default;
virtual WalletBackend& activeBackend() = 0;
virtual const WalletBackend& activeBackend() const = 0;
};
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,179 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#ifndef DRAGONX_LITE_BUILD
#define DRAGONX_LITE_BUILD 0
#endif
#ifndef DRAGONX_ENABLE_EMBEDDED_DAEMON
#define DRAGONX_ENABLE_EMBEDDED_DAEMON 1
#endif
#ifndef DRAGONX_ENABLE_LITE_BACKEND
#define DRAGONX_ENABLE_LITE_BACKEND 0
#endif
namespace dragonx {
namespace wallet {
enum class WalletBuildKind {
FullNode,
Lite
};
enum class WalletBackendKind {
FullNodeRpc,
LiteClient
};
enum class WalletUiSurface {
Overview,
Send,
Receive,
History,
Mining,
Market,
Console,
Peers,
Explorer,
Settings,
BootstrapDownload,
SetupWizard,
NodeSettings
};
struct WalletCapabilities {
WalletBuildKind buildKind = WalletBuildKind::FullNode;
WalletBackendKind backendKind = WalletBackendKind::FullNodeRpc;
bool fullNodeRpcAvailable = true;
bool embeddedDaemonAvailable = true;
bool fullNodePagesAvailable = true;
bool fullNodeLifecycleActionsAvailable = true;
bool soloMiningAvailable = true;
bool poolMiningAvailable = true;
bool liteBackendAvailable = false;
bool lightwalletdNetworkAvailable = false;
bool liteWalletLifecycleAvailable = false;
bool liteWalletSyncAvailable = false;
bool liteWalletSendAvailable = false;
};
constexpr bool isLiteBuild(WalletBuildKind buildKind)
{
return buildKind == WalletBuildKind::Lite;
}
constexpr bool isLiteBuild(const WalletCapabilities& capabilities)
{
return isLiteBuild(capabilities.buildKind);
}
constexpr bool isFullNodeBuild(WalletBuildKind buildKind)
{
return buildKind == WalletBuildKind::FullNode;
}
constexpr WalletCapabilities makeWalletCapabilities(
WalletBuildKind buildKind,
bool embeddedDaemonCompiled,
bool liteBackendLinked)
{
const bool liteBuild = isLiteBuild(buildKind);
const bool fullNodeBuild = !liteBuild;
const bool liteBackendAvailable = liteBuild && liteBackendLinked;
return WalletCapabilities{
buildKind,
liteBuild ? WalletBackendKind::LiteClient : WalletBackendKind::FullNodeRpc,
fullNodeBuild,
fullNodeBuild && embeddedDaemonCompiled,
fullNodeBuild,
fullNodeBuild,
fullNodeBuild,
true,
liteBackendAvailable,
liteBackendAvailable,
liteBackendAvailable,
liteBackendAvailable,
liteBackendAvailable
};
}
constexpr WalletBuildKind currentWalletBuildKind()
{
return DRAGONX_LITE_BUILD ? WalletBuildKind::Lite : WalletBuildKind::FullNode;
}
constexpr WalletCapabilities currentWalletCapabilities()
{
return makeWalletCapabilities(
currentWalletBuildKind(),
DRAGONX_ENABLE_EMBEDDED_DAEMON != 0,
DRAGONX_ENABLE_LITE_BACKEND != 0);
}
constexpr bool supportsEmbeddedDaemon(const WalletCapabilities& capabilities)
{
return capabilities.embeddedDaemonAvailable;
}
constexpr bool supportsFullNodeLifecycleActions(const WalletCapabilities& capabilities)
{
return capabilities.fullNodeLifecycleActionsAvailable;
}
constexpr bool supportsSoloMining(const WalletCapabilities& capabilities)
{
return capabilities.soloMiningAvailable;
}
constexpr bool supportsPoolMining(const WalletCapabilities& capabilities)
{
return capabilities.poolMiningAvailable;
}
constexpr bool supportsLiteBackend(const WalletCapabilities& capabilities)
{
return capabilities.liteBackendAvailable;
}
constexpr bool supportsWalletDataBackend(const WalletCapabilities& capabilities)
{
return capabilities.fullNodeRpcAvailable || capabilities.liteWalletSyncAvailable;
}
constexpr bool isUiSurfaceAvailable(const WalletCapabilities& capabilities,
WalletUiSurface surface)
{
switch (surface) {
case WalletUiSurface::Console:
case WalletUiSurface::Peers:
case WalletUiSurface::Explorer:
return capabilities.fullNodePagesAvailable;
case WalletUiSurface::BootstrapDownload:
case WalletUiSurface::SetupWizard:
case WalletUiSurface::NodeSettings:
return capabilities.fullNodeLifecycleActionsAvailable;
default:
return true;
}
}
constexpr bool uiSurfaceNeedsWalletData(WalletUiSurface surface)
{
switch (surface) {
case WalletUiSurface::Overview:
case WalletUiSurface::Send:
case WalletUiSurface::Receive:
case WalletUiSurface::History:
return true;
default:
return false;
}
}
} // namespace wallet
} // namespace dragonx

View File

@@ -0,0 +1,27 @@
# HushChat Capture Manifest
The capture manifest is a redacted provenance file for a staged directory of real, disposable, non-sensitive SilentDragonXLite HushChat vectors. It does not replace fixture validation; it records that the staged directory was handled under the safety rules required before the Batch 11 strict replacement dry run.
Copy `templates/capture-manifest.template.json` into the staged fixture directory as `capture-manifest.json`, then change `status` to `staged` and each category `status` to `ready`. Keep the file limited to provenance, handling flags, category filenames, and the dry-run command.
Validate only the manifest metadata with:
```sh
./build/bin/HushChatFixtureCheck --validate-capture-manifest /path/to/staged/hushchat-fixtures
```
The validator accepts a directory containing `capture-manifest.json` or a direct manifest file path. It prints only a redacted report with schema status, category coverage, handling-flag counts, basenames, and error names.
The manifest must include:
- schema `dragonx.hushchat.capture-manifest.v1`
- status `staged`
- a redacted manifest id
- staged fixture directory name
- dry-run command containing `HushChatFixtureCheck --replacement-dry-run`
- provenance fields for source client, source client version or commit, capture date, network, and capture method
- all required handling flags set to `true`
- exactly one entry for each required fixture category
The manifest must not include passphrases, plaintext, memo contents, private keys, wallet files, ciphertext byte dumps, stored chat key fields, public-key fields, secretstream headers, derived keys, session keys, or any fixture object. The manifest validator rejects known prohibited field names, but it is still a metadata guard rather than a secret scanner.
After the manifest validates, run the strict replacement dry run against the same staged directory. Pending checked-in fixtures must not be replaced until both commands succeed.

View File

@@ -0,0 +1,40 @@
# HushChat Fixture Import Checklist
This checklist is for replacing the pending placeholders with real, non-sensitive SilentDragonXLite compatibility vectors.
Do not commit passphrases, plaintext, memo contents from real wallets, private keys, wallet files, or arbitrary ciphertext dumps. Use only disposable test wallets and non-sensitive test messages. The checked-in ready files must contain only the schema fields required by the loader.
Required categories:
- `incoming_memo`: received encrypted Memo vector, expected `ClientRx`
- `outgoing_memo`: outgoing-history encrypted Memo vector, expected `ServerTx`
- `seed_public_key_projection`: vector proving the SDXL UTF-8-hex seed projection matches the recorded local public key
- `corrupted_auth_failure`: structurally valid Memo vector reserved for future authentication-failure verification
- `cont_exclusion`: contact request vector that remains excluded from encrypted Memo decrypt preparation
Capture rules:
- Use disposable SilentDragonXLite wallets only.
- Use a fixed non-sensitive test phrase and record only its hash if a plaintext expectation is needed.
- Do not store decrypted message text in fixture files.
- Do not include private keys, viewing keys, spending keys, wallet seed phrases, or wallet database contents.
- Keep the top-level `schema` value as `dragonx.hushchat.compat-fixture.v1`.
- Change `status` from `pending` to `ready` only when the fixture has the full `fixture` object documented in the protocol spec.
- Preserve one fixture file per required category.
- For every ready non-`Cont` vector, the stored SDXL chat key string must project to the declared local public key using SDXL's first-32-UTF-8-bytes seed behavior.
- The projection check records only byte lengths and match status; it must not write passphrases, plaintext, derived secret keys, memo contents, or ciphertext bytes.
- A ready `corrupted_auth_failure` vector must be structurally valid through the same loader, verifier, and projection checks, then marked as requiring a future secretstream authentication failure.
- The corrupted-auth marker is not a decrypt result and is not an authentication result.
- Run the strict replacement dry-run report against a staged directory before copying any ready files over the checked-in pending placeholders.
- The dry-run report must remain redacted; it may contain category names, basenames, status/error names, boolean flags, and counts only.
- Add `capture-manifest.json` to the staged directory from `templates/capture-manifest.template.json` and validate it before strict replacement dry-run checks.
- The capture manifest records provenance, handling flags, category filenames, and dry-run instructions only; it must not contain fixture objects or sensitive fields.
Verification flow:
```sh
./build/bin/HushChatFixtureCheck --allow-pending tests/fixtures/hushchat
./build/bin/HushChatFixtureCheck --validate-capture-manifest /path/to/staged/hushchat-fixtures
./build/bin/HushChatFixtureCheck --replacement-dry-run /path/to/staged/hushchat-fixtures
./build/bin/HushChatFixtureCheck tests/fixtures/hushchat
```
The first command is for the current scaffold state and allows pending files. The manifest command validates only redacted staged-directory metadata. The dry-run command is strict, refuses `--allow-pending`, performs no file replacement, and must fail until the staged directory contains all five real ready vectors with no pending, malformed, mismatched, projection-failed, auth-not-ready, or Cont-not-excluded entries. The final command is the post-copy strict check on the checked-in fixture directory; it must report `future_auth_required=1` and `auth_structural_ready=1`. A ready import is acceptable only when the manifest command and both strict fixture commands exit successfully.

31
tests/fixtures/hushchat/README.md vendored Normal file
View File

@@ -0,0 +1,31 @@
# HushChat Compatibility Fixtures
This directory contains the checked-in fixture-file schema for non-sensitive HushChat compatibility vectors.
The current files are intentionally marked `pending`. They define the exact vector categories and required fields without pretending that real SilentDragonXLite compatibility data has been captured yet.
Use the developer checker after building:
```sh
./build/bin/HushChatFixtureCheck --allow-pending tests/fixtures/hushchat
```
The command above is expected to pass while files are still pending. Before replacing pending placeholders with real `ready` vectors, run the same command without `--allow-pending`; it exits successfully only when all five required categories are supplied exactly once, pass the fixture loader/import checklist, satisfy the seed/public-key projection verifier, and mark the corrupted-auth vector as structurally ready for a future authentication-failure check.
For a strict replacement dry run against a staged directory of real vectors, use:
```sh
./build/bin/HushChatFixtureCheck --replacement-dry-run /path/to/staged/hushchat-fixtures
```
The dry-run mode refuses pending vectors and prints only a redacted replacement report with categories, basenames, status/error names, boolean flags, and aggregate counts. It does not copy files or print key material, memo contents, plaintext, ciphertext bytes, or hashes.
Before running the strict replacement dry run on real staged vectors, add a redacted capture manifest and validate it:
```sh
./build/bin/HushChatFixtureCheck --validate-capture-manifest /path/to/staged/hushchat-fixtures
```
The manifest template lives at `templates/capture-manifest.template.json`. See `CAPTURE_MANIFEST.md` for the metadata rules.
See `IMPORT_CHECKLIST.md` for the capture rules.

View File

@@ -0,0 +1,7 @@
{
"schema": "dragonx.hushchat.compat-fixture.v1",
"status": "pending",
"kind": "cont_exclusion",
"id": "sdxl-cont-exclusion",
"pending_reason": "Needs a non-sensitive Cont fixture proving contact requests remain outside encrypted Memo decrypt handling."
}

View File

@@ -0,0 +1,7 @@
{
"schema": "dragonx.hushchat.compat-fixture.v1",
"status": "pending",
"kind": "corrupted_auth_failure",
"id": "sdxl-corrupted-auth-failure",
"pending_reason": "Needs a non-sensitive Memo vector that passes structural Batch 1-6 validation but is expected to fail future secretstream authentication."
}

View File

@@ -0,0 +1,7 @@
{
"schema": "dragonx.hushchat.compat-fixture.v1",
"status": "pending",
"kind": "incoming_memo",
"id": "sdxl-incoming-memo",
"pending_reason": "Needs a non-sensitive incoming Memo vector captured from SilentDragonXLite with stored chat key hex, public keys, header memo, ciphertext memo, expected role, byte lengths, and plaintext hash only."
}

View File

@@ -0,0 +1,7 @@
{
"schema": "dragonx.hushchat.compat-fixture.v1",
"status": "pending",
"kind": "outgoing_memo",
"id": "sdxl-outgoing-memo",
"pending_reason": "Needs a non-sensitive outgoing-history Memo vector captured from SilentDragonXLite with stored chat key hex, local and peer public keys, header memo, ciphertext memo, expected server_tx role, byte lengths, and plaintext hash only."
}

View File

@@ -0,0 +1,7 @@
{
"schema": "dragonx.hushchat.compat-fixture.v1",
"status": "pending",
"kind": "seed_public_key_projection",
"id": "sdxl-seed-public-key-projection",
"pending_reason": "Needs a non-sensitive vector proving the SilentDragonXLite UTF-8-hex seed projection produces the observed local public key without storing the original passphrase or plaintext."
}

View File

@@ -0,0 +1,54 @@
{
"schema": "dragonx.hushchat.capture-manifest.v1",
"status": "template",
"id": "replace-with-redacted-capture-manifest-id",
"fixture_directory": "replace-with-staged-fixture-directory",
"dry_run_command": "./build/bin/HushChatFixtureCheck --replacement-dry-run replace-with-staged-fixture-directory",
"provenance": {
"source_client": "SilentDragonXLite",
"source_client_version": "replace-with-release-or-commit",
"capture_date": "YYYY-MM-DD",
"network": "replace-with-disposable-test-network",
"capture_method": "replace-with-redacted-capture-procedure-name"
},
"handling": {
"disposable_wallets_only": true,
"non_sensitive_vectors_only": true,
"no_passphrases": true,
"no_plaintext": true,
"no_memo_contents": true,
"no_private_keys": true,
"no_wallet_files": true,
"no_ciphertext_byte_dumps": true,
"no_derived_keys": true,
"no_session_keys": true,
"redacted_report_only": true
},
"categories": [
{
"kind": "incoming_memo",
"staged_filename": "incoming-memo.ready.json",
"status": "template"
},
{
"kind": "outgoing_memo",
"staged_filename": "outgoing-memo.ready.json",
"status": "template"
},
{
"kind": "seed_public_key_projection",
"staged_filename": "seed-public-key-projection.ready.json",
"status": "template"
},
{
"kind": "corrupted_auth_failure",
"staged_filename": "corrupted-auth-failure.ready.json",
"status": "template"
},
{
"kind": "cont_exclusion",
"staged_filename": "cont-exclusion.ready.json",
"status": "template"
}
]
}

View File

@@ -0,0 +1,60 @@
{
"schema": "dragonx.lite.release-package-manifest.v1",
"source": "generated-package-manifest-fixture",
"generated": true,
"packages": [
{
"name": "ObsidianDragon-full-linux-fixture",
"build_kind": "full-node",
"format": "linux-zip",
"pool_mining_supported": true,
"solo_mining_available": true,
"full_node_lifecycle_actions_available": true,
"expect": {
"dragonxd": true,
"dragonx_cli": true,
"dragonx_tx": true,
"sapling_params": true,
"asmap": true,
"xmrig_when_available": true
},
"assets": [
{ "kind": "app-binary", "path": "ObsidianDragon", "present": true, "executable": true },
{ "kind": "dragonxd", "path": "usr/bin/dragonxd", "present": true, "executable": true },
{ "kind": "dragonx-cli", "path": "usr/bin/dragonx-cli", "present": true, "executable": true },
{ "kind": "dragonx-tx", "path": "usr/bin/dragonx-tx", "present": true, "executable": true },
{ "kind": "sapling-spend.params", "path": "usr/bin/sapling-spend.params", "present": true, "executable": false },
{ "kind": "sapling-output.params", "path": "usr/bin/sapling-output.params", "present": true, "executable": false },
{ "kind": "asmap.dat", "path": "usr/bin/asmap.dat", "present": true, "executable": false },
{ "kind": "xmrig", "path": "usr/bin/xmrig", "present": true, "executable": true },
{ "kind": "resources", "path": "usr/share/ObsidianDragon/res", "present": true, "executable": false }
]
},
{
"name": "ObsidianDragonLite-linux-fixture",
"build_kind": "lite",
"format": "linux-zip",
"pool_mining_supported": true,
"solo_mining_available": false,
"full_node_lifecycle_actions_available": false,
"assets": [
{ "kind": "app-binary", "path": "ObsidianDragonLite", "present": true, "executable": true },
{ "kind": "xmrig", "path": "xmrig", "present": true, "executable": true },
{ "kind": "resources", "path": "res", "present": true, "executable": false }
]
},
{
"name": "ObsidianDragonLite-windows-fixture",
"build_kind": "lite",
"format": "windows-zip",
"pool_mining_supported": true,
"solo_mining_available": false,
"full_node_lifecycle_actions_available": false,
"assets": [
{ "kind": "app-binary", "path": "ObsidianDragonLite.exe", "present": true, "executable": true },
{ "kind": "other", "path": "tools/xmrig.exe", "present": true, "executable": true },
{ "kind": "resources", "path": "res", "present": true, "executable": false }
]
}
]
}

107
tests/fixtures/lite/result_parsers.json vendored Normal file
View File

@@ -0,0 +1,107 @@
{
"info": {
"chain_name": "dragonx",
"version": "0.1.0",
"vendor": "SilentDragonXLite",
"latest_block_height": 250001,
"difficulty": 42,
"longestchain": 250003,
"notarized": 249990
},
"height_object": {
"height": 250004
},
"height_number": 250005,
"balance": {
"tbalance": 125000000,
"zbalance": 230000000,
"unconfirmed": 5000000,
"verified_zbalance": 180000000,
"spendable_zbalance": 170000000
},
"addresses": {
"z_addresses": [
"zs1parserfixture0000000000000000000000000000000000000000000000000000",
"zs1parserfixture1111111111111111111111111111111111111111111111111111"
],
"t_addresses": [
"RParserFixtureTransparentAddress0000000000001"
]
},
"notes": {
"unspent_notes": [
{
"address": "zs1parserfixture0000000000000000000000000000000000000000000000000000",
"created_in_block": 249900,
"created_in_txid": "note-txid-1",
"value": 100000000,
"spent": false,
"unconfirmed_spent": false
}
],
"utxos": [
{
"address": "RParserFixtureTransparentAddress0000000000001",
"created_in_block": 249901,
"created_in_txid": "utxo-txid-1",
"value": 25000000,
"spent": false,
"unconfirmed_spent": true
}
],
"pending_notes": [
{
"address": "zs1parserfixture1111111111111111111111111111111111111111111111111111",
"created_in_block": 0,
"created_in_txid": "pending-note-txid-1",
"value": 30000000,
"spent": false,
"unconfirmed_spent": false
}
],
"pending_utxos": [
{
"address": "RParserFixtureTransparentAddress0000000000001",
"created_in_block": 0,
"created_in_txid": "pending-utxo-txid-1",
"value": 12000000,
"spent": false,
"unconfirmed_spent": false
}
]
},
"list": [
{
"txid": "sent-txid-1",
"datetime": 1710000000,
"block_height": 249950,
"unconfirmed": false,
"outgoing_metadata": [
{
"address": "zs1recipientfixture0000000000000000000000000000000000000000000000",
"value": 75000000,
"memo": "outgoing memo"
},
{
"address": "RRecipientTransparentFixture000000000000001",
"value": 5000000,
"memo": "transparent memo"
}
]
},
{
"txid": "receive-txid-1",
"datetime": 1710001000,
"block_height": 0,
"unconfirmed": true,
"address": "zs1parserfixture1111111111111111111111111111111111111111111111111111",
"amount": 42000000,
"memo": "incoming memo",
"position": 2
}
],
"syncstatus": {
"synced_blocks": 125002,
"total_blocks": 250004
}
}

View File

@@ -0,0 +1,227 @@
#include "chat/chat_protocol.h"
#include <algorithm>
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
namespace {
using dragonx::chat::HushChatCompatibilityFixtureImportCandidate;
using dragonx::chat::HushChatCompatibilityFixtureKind;
std::string baseName(const std::string& path)
{
return fs::path(path).filename().string();
}
std::vector<std::string> jsonFilesInDirectory(const fs::path& directory)
{
std::vector<std::string> paths;
if (!fs::exists(directory) || !fs::is_directory(directory)) return paths;
for (const auto& entry : fs::directory_iterator(directory)) {
if (!entry.is_regular_file()) continue;
if (entry.path().extension() != ".json") continue;
paths.push_back(entry.path().string());
}
std::sort(paths.begin(), paths.end());
return paths;
}
HushChatCompatibilityFixtureKind expectedKindFromPath(const std::string& path)
{
const std::string name = baseName(path);
if (name.find("outgoing") != std::string::npos) return HushChatCompatibilityFixtureKind::OutgoingMemo;
if (name.find("seed") != std::string::npos) return HushChatCompatibilityFixtureKind::SeedPublicKeyProjection;
if (name.find("corrupted") != std::string::npos) return HushChatCompatibilityFixtureKind::CorruptedAuthFailure;
if (name.find("cont") != std::string::npos) return HushChatCompatibilityFixtureKind::ContactExclusion;
return HushChatCompatibilityFixtureKind::IncomingMemo;
}
void printUsage(const char* program)
{
std::cerr << "Usage: " << program
<< " [--allow-pending] [--replacement-dry-run] [--validate-capture-manifest]"
<< " <fixture-file-or-directory>...\n";
}
const char* boolText(bool value)
{
return value ? "1" : "0";
}
void printReplacementDryRun(const dragonx::chat::HushChatCompatibilityFixtureReplacementDryRunResult& dryRun)
{
std::cout << "HushChat fixture replacement dry run: "
<< (dryRun.ok ? "Ready" : "Refused") << "\n";
std::cout << "error=" << dryRun.error_name
<< " dry_run_only=" << boolText(dryRun.dry_run_only)
<< " redacted=" << boolText(dryRun.redacted_report)
<< " would_replace=" << boolText(dryRun.would_replace)
<< " replacement_refused=" << boolText(dryRun.replacement_refused) << "\n";
std::cout << "required=" << dryRun.required_count
<< " supplied=" << dryRun.supplied_count
<< " missing=" << dryRun.missing_count
<< " verified=" << dryRun.verified_count
<< " seed_projected=" << dryRun.seed_projection_verified_count
<< " future_auth_required=" << dryRun.future_auth_failure_required_count
<< " auth_structural_ready=" << dryRun.auth_failure_structural_ready_count
<< " cont_excluded=" << dryRun.excluded_count
<< " pending=" << dryRun.pending_count
<< " rejected=" << dryRun.rejected_count << "\n";
for (const auto& item : dryRun.report_items) {
std::cout << dragonx::chat::hushChatCompatibilityFixtureKindName(item.expected_kind)
<< " decision=" << (item.replacement_eligible ? "ready" : "refused")
<< " error=" << item.error_name
<< " supplied=" << boolText(item.supplied)
<< " pending=" << boolText(item.pending)
<< " seed_projected=" << boolText(item.seed_projection_verified)
<< " future_auth_required=" << boolText(item.future_auth_failure_required)
<< " auth_structural_ready=" << boolText(item.structurally_ready_for_future_auth_check)
<< " cont_excluded=" << boolText(item.cont_excluded)
<< " decrypted=" << boolText(item.decrypted)
<< " authenticated=" << boolText(item.authenticated);
if (item.supplied) std::cout << " file=" << baseName(item.path);
std::cout << "\n";
}
}
std::string captureManifestPathFromInput(const std::string& input)
{
fs::path path(input);
if (fs::exists(path) && fs::is_directory(path)) return (path / "capture-manifest.json").string();
return input;
}
void printCaptureManifestValidation(const dragonx::chat::HushChatCaptureManifestValidationResult& validation)
{
std::cout << "HushChat capture manifest: " << (validation.ok ? "Ready" : "Refused") << "\n";
std::cout << "error=" << validation.error_name
<< " redacted=" << boolText(validation.redacted_report)
<< " provenance_only=" << boolText(validation.validates_provenance_only)
<< " no_sensitive_material=" << boolText(validation.no_sensitive_material_declared)
<< " dry_run_instruction=" << boolText(validation.has_dry_run_command);
if (!validation.manifest_path.empty()) std::cout << " manifest=" << baseName(validation.manifest_path);
if (!validation.fixture_directory.empty()) std::cout << " fixture_dir=" << baseName(validation.fixture_directory);
std::cout << "\n";
std::cout << "required=" << validation.required_count
<< " declared=" << validation.declared_count
<< " missing=" << validation.missing_count
<< " duplicate=" << validation.duplicate_count
<< " handling_flags=" << validation.handling_flag_count
<< " prohibited=" << validation.prohibited_field_count << "\n";
for (const auto& category : validation.categories) {
std::cout << dragonx::chat::hushChatCompatibilityFixtureKindName(category.kind)
<< " declared=" << boolText(category.declared);
if (!category.staged_filename.empty()) std::cout << " file=" << baseName(category.staged_filename);
std::cout << "\n";
}
}
} // namespace
int main(int argc, char** argv)
{
bool allowPending = false;
bool replacementDryRun = false;
bool validateCaptureManifest = false;
std::vector<std::string> inputPaths;
for (int index = 1; index < argc; ++index) {
const std::string arg = argv[index];
if (arg == "--allow-pending") {
allowPending = true;
} else if (arg == "--replacement-dry-run") {
replacementDryRun = true;
} else if (arg == "--validate-capture-manifest") {
validateCaptureManifest = true;
} else if (arg == "--help" || arg == "-h") {
printUsage(argv[0]);
return 0;
} else {
inputPaths.push_back(arg);
}
}
if (inputPaths.empty()) {
printUsage(argv[0]);
return 2;
}
if (validateCaptureManifest && (allowPending || replacementDryRun)) {
std::cerr << "Capture manifest validation is separate from fixture and replacement checks.\n";
return 2;
}
if (replacementDryRun && allowPending) {
std::cerr << "Replacement dry run is strict; remove --allow-pending.\n";
return 2;
}
if (validateCaptureManifest) {
bool allValid = true;
for (const auto& input : inputPaths) {
const auto validation = dragonx::chat::loadHushChatCaptureManifestFile(
captureManifestPathFromInput(input),
true);
printCaptureManifestValidation(validation);
if (!validation.ok) allValid = false;
}
return allValid ? 0 : 1;
}
std::vector<std::string> paths;
for (const auto& input : inputPaths) {
fs::path path(input);
if (fs::exists(path) && fs::is_directory(path)) {
auto directoryFiles = jsonFilesInDirectory(path);
paths.insert(paths.end(), directoryFiles.begin(), directoryFiles.end());
} else {
paths.push_back(input);
}
}
std::vector<HushChatCompatibilityFixtureImportCandidate> candidates;
candidates.reserve(paths.size());
for (const auto& path : paths) {
candidates.push_back(HushChatCompatibilityFixtureImportCandidate{expectedKindFromPath(path), path});
}
if (replacementDryRun) {
const auto dryRun = dragonx::chat::inspectHushChatCompatibilityFixtureReplacementDryRun(candidates, true);
printReplacementDryRun(dryRun);
return dryRun.ok ? 0 : 1;
}
const auto checklist = dragonx::chat::inspectHushChatCompatibilityFixtureImportChecklist(candidates, true);
std::cout << "HushChat fixture import checklist: " << checklist.error_name << "\n";
std::cout << "required=" << checklist.required_count
<< " supplied=" << checklist.supplied_count
<< " missing=" << checklist.missing_count
<< " verified=" << checklist.verified_count
<< " seed_projected=" << checklist.seed_projection_verified_count
<< " future_auth_required=" << checklist.future_auth_failure_required_count
<< " auth_structural_ready=" << checklist.auth_failure_structural_ready_count
<< " cont_excluded=" << checklist.excluded_count
<< " pending=" << checklist.pending_count
<< " rejected=" << checklist.rejected_count << "\n";
for (const auto& item : checklist.items) {
std::cout << dragonx::chat::hushChatCompatibilityFixtureKindName(item.expected_kind)
<< " " << item.error_name;
if (item.future_auth_failure_required) std::cout << " FutureAuthFailureRequired";
if (item.structurally_ready_for_future_auth_check) std::cout << " StructurallyReadyForFutureAuthCheck";
if (item.supplied) std::cout << " " << baseName(item.path);
std::cout << "\n";
}
if (checklist.replacement_ready) return 0;
if (allowPending && checklist.supplied_count == checklist.required_count &&
checklist.missing_count == 0 && checklist.pending_count > 0 && checklist.rejected_count == 0) return 0;
return 1;
}