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:
769
scripts/build-lite-backend-artifact.sh
Executable file
769
scripts/build-lite-backend-artifact.sh
Executable 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
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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
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
586
src/chat/chat_protocol.h
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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@"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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]);
|
||||
|
||||
419
src/wallet/lite_backend_artifact_contract.cpp
Normal file
419
src/wallet/lite_backend_artifact_contract.cpp
Normal 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
|
||||
195
src/wallet/lite_backend_artifact_contract.h
Normal file
195
src/wallet/lite_backend_artifact_contract.h
Normal 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
|
||||
925
src/wallet/lite_backend_artifact_resolver.cpp
Normal file
925
src/wallet/lite_backend_artifact_resolver.cpp
Normal 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
|
||||
414
src/wallet/lite_backend_artifact_resolver.h
Normal file
414
src/wallet/lite_backend_artifact_resolver.h
Normal 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
|
||||
17669
src/wallet/lite_bridge_runtime.cpp
Normal file
17669
src/wallet/lite_bridge_runtime.cpp
Normal file
File diff suppressed because it is too large
Load Diff
7268
src/wallet/lite_bridge_runtime.h
Normal file
7268
src/wallet/lite_bridge_runtime.h
Normal file
File diff suppressed because it is too large
Load Diff
186
src/wallet/lite_client_bridge.cpp
Normal file
186
src/wallet/lite_client_bridge.cpp
Normal 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
|
||||
84
src/wallet/lite_client_bridge.h
Normal file
84
src/wallet/lite_client_bridge.h
Normal 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
|
||||
309
src/wallet/lite_connection_service.cpp
Normal file
309
src/wallet/lite_connection_service.cpp
Normal 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
|
||||
123
src/wallet/lite_connection_service.h
Normal file
123
src/wallet/lite_connection_service.h
Normal 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
|
||||
570
src/wallet/lite_result_parsers.cpp
Normal file
570
src/wallet/lite_result_parsers.cpp
Normal 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
|
||||
169
src/wallet/lite_result_parsers.h
Normal file
169
src/wallet/lite_result_parsers.h
Normal 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
|
||||
371
src/wallet/lite_sync_service.cpp
Normal file
371
src/wallet/lite_sync_service.cpp
Normal 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
|
||||
162
src/wallet/lite_sync_service.h
Normal file
162
src/wallet/lite_sync_service.h
Normal 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
|
||||
172
src/wallet/lite_wallet_app_refresh_coordinator.cpp
Normal file
172
src/wallet/lite_wallet_app_refresh_coordinator.cpp
Normal 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
|
||||
96
src/wallet/lite_wallet_app_refresh_coordinator.h
Normal file
96
src/wallet/lite_wallet_app_refresh_coordinator.h
Normal 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
|
||||
197
src/wallet/lite_wallet_app_refresh_orchestrator.cpp
Normal file
197
src/wallet/lite_wallet_app_refresh_orchestrator.cpp
Normal 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
|
||||
109
src/wallet/lite_wallet_app_refresh_orchestrator.h
Normal file
109
src/wallet/lite_wallet_app_refresh_orchestrator.h
Normal 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
|
||||
409
src/wallet/lite_wallet_gateway.cpp
Normal file
409
src/wallet/lite_wallet_gateway.cpp
Normal 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
|
||||
163
src/wallet/lite_wallet_gateway.h
Normal file
163
src/wallet/lite_wallet_gateway.h
Normal 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
|
||||
397
src/wallet/lite_wallet_lifecycle_service.cpp
Normal file
397
src/wallet/lite_wallet_lifecycle_service.cpp
Normal 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
|
||||
152
src/wallet/lite_wallet_lifecycle_service.h
Normal file
152
src/wallet/lite_wallet_lifecycle_service.h
Normal 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
|
||||
440
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
Normal file
440
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
Normal 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
|
||||
146
src/wallet/lite_wallet_lifecycle_ui_adapter.h
Normal file
146
src/wallet/lite_wallet_lifecycle_ui_adapter.h
Normal 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
|
||||
205
src/wallet/lite_wallet_refresh_readiness_policy.cpp
Normal file
205
src/wallet/lite_wallet_refresh_readiness_policy.cpp
Normal 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
|
||||
106
src/wallet/lite_wallet_refresh_readiness_policy.h
Normal file
106
src/wallet/lite_wallet_refresh_readiness_policy.h
Normal 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
|
||||
189
src/wallet/lite_wallet_refresh_service.cpp
Normal file
189
src/wallet/lite_wallet_refresh_service.cpp
Normal 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
|
||||
92
src/wallet/lite_wallet_refresh_service.h
Normal file
92
src/wallet/lite_wallet_refresh_service.h
Normal 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
|
||||
407
src/wallet/lite_wallet_server_lifecycle_readiness.cpp
Normal file
407
src/wallet/lite_wallet_server_lifecycle_readiness.cpp
Normal 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
|
||||
197
src/wallet/lite_wallet_server_lifecycle_readiness.h
Normal file
197
src/wallet/lite_wallet_server_lifecycle_readiness.h
Normal 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
|
||||
463
src/wallet/lite_wallet_server_selection_adapter.cpp
Normal file
463
src/wallet/lite_wallet_server_selection_adapter.cpp
Normal 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
|
||||
161
src/wallet/lite_wallet_server_selection_adapter.h
Normal file
161
src/wallet/lite_wallet_server_selection_adapter.h
Normal 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
|
||||
118
src/wallet/lite_wallet_state_apply_executor.cpp
Normal file
118
src/wallet/lite_wallet_state_apply_executor.cpp
Normal 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
|
||||
71
src/wallet/lite_wallet_state_apply_executor.h
Normal file
71
src/wallet/lite_wallet_state_apply_executor.h
Normal 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
|
||||
405
src/wallet/lite_wallet_state_apply_plan.cpp
Normal file
405
src/wallet/lite_wallet_state_apply_plan.cpp
Normal 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
|
||||
98
src/wallet/lite_wallet_state_apply_plan.h
Normal file
98
src/wallet/lite_wallet_state_apply_plan.h
Normal 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
|
||||
217
src/wallet/lite_wallet_state_mapper.cpp
Normal file
217
src/wallet/lite_wallet_state_mapper.cpp
Normal 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
|
||||
139
src/wallet/lite_wallet_state_mapper.h
Normal file
139
src/wallet/lite_wallet_state_mapper.h
Normal 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
|
||||
344
src/wallet/lite_wallet_sync_app_refresh_integration.cpp
Normal file
344
src/wallet/lite_wallet_sync_app_refresh_integration.cpp
Normal 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
|
||||
172
src/wallet/lite_wallet_sync_app_refresh_integration.h
Normal file
172
src/wallet/lite_wallet_sync_app_refresh_integration.h
Normal 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
|
||||
461
src/wallet/lite_wallet_sync_execution_readiness.cpp
Normal file
461
src/wallet/lite_wallet_sync_execution_readiness.cpp
Normal 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
|
||||
238
src/wallet/lite_wallet_sync_execution_readiness.h
Normal file
238
src/wallet/lite_wallet_sync_execution_readiness.h
Normal 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
|
||||
93
src/wallet/wallet_backend.h
Normal file
93
src/wallet/wallet_backend.h
Normal 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
|
||||
179
src/wallet/wallet_capabilities.h
Normal file
179
src/wallet/wallet_capabilities.h
Normal 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
|
||||
27
tests/fixtures/hushchat/CAPTURE_MANIFEST.md
vendored
Normal file
27
tests/fixtures/hushchat/CAPTURE_MANIFEST.md
vendored
Normal 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.
|
||||
40
tests/fixtures/hushchat/IMPORT_CHECKLIST.md
vendored
Normal file
40
tests/fixtures/hushchat/IMPORT_CHECKLIST.md
vendored
Normal 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
31
tests/fixtures/hushchat/README.md
vendored
Normal 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.
|
||||
7
tests/fixtures/hushchat/cont-exclusion.pending.json
vendored
Normal file
7
tests/fixtures/hushchat/cont-exclusion.pending.json
vendored
Normal 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."
|
||||
}
|
||||
7
tests/fixtures/hushchat/corrupted-auth-failure.pending.json
vendored
Normal file
7
tests/fixtures/hushchat/corrupted-auth-failure.pending.json
vendored
Normal 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."
|
||||
}
|
||||
7
tests/fixtures/hushchat/incoming-memo.pending.json
vendored
Normal file
7
tests/fixtures/hushchat/incoming-memo.pending.json
vendored
Normal 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."
|
||||
}
|
||||
7
tests/fixtures/hushchat/outgoing-memo.pending.json
vendored
Normal file
7
tests/fixtures/hushchat/outgoing-memo.pending.json
vendored
Normal 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."
|
||||
}
|
||||
7
tests/fixtures/hushchat/seed-public-key-projection.pending.json
vendored
Normal file
7
tests/fixtures/hushchat/seed-public-key-projection.pending.json
vendored
Normal 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."
|
||||
}
|
||||
54
tests/fixtures/hushchat/templates/capture-manifest.template.json
vendored
Normal file
54
tests/fixtures/hushchat/templates/capture-manifest.template.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
60
tests/fixtures/lite/release_package_manifest.json
vendored
Normal file
60
tests/fixtures/lite/release_package_manifest.json
vendored
Normal 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
107
tests/fixtures/lite/result_parsers.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
227
tools/hushchat_fixture_check.cpp
Normal file
227
tools/hushchat_fixture_check.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user