diff --git a/.env.example b/.env.example index f3527862f..0a9bd218b 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ MAINNET_core_rpc_port=9998 MAINNET_core_rpc_user=dashrpc MAINNET_core_rpc_password=password MAINNET_insight_api_url=https://insight.dash.org/insight-api +MAINNET_core_zmq_endpoint=tcp://127.0.0.1:23708 MAINNET_show_in_ui=true MAINNET_developer_mode=true @@ -13,6 +14,7 @@ TESTNET_core_rpc_port=19998 TESTNET_core_rpc_user=dashrpc TESTNET_core_rpc_password=password TESTNET_insight_api_url=https://testnet-insight.dash.org/insight-api +TESTNET_core_zmq_endpoint=tcp://127.0.0.1:23709 TESTNET_show_in_ui=true TESTNET_developer_mode=false @@ -22,6 +24,7 @@ DEVNET_core_rpc_port=29998 DEVNET_core_rpc_user=dashrpc DEVNET_core_rpc_password=password DEVNET_insight_api_url= +DEVNET_core_zmq_endpoint=tcp://127.0.0.1:23710 DEVNET_show_in_ui=true DEVNET_developer_mode=false @@ -31,4 +34,5 @@ LOCAL_core_rpc_port=20302 LOCAL_core_rpc_user=dashmate LOCAL_core_rpc_password=password LOCAL_insight_api_url=http://localhost:3001/insight-api +LOCAL_core_zmq_endpoint=tcp://127.0.0.1:20302 LOCAL_show_in_ui=true \ No newline at end of file diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index ba81213f4..47348b757 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -34,7 +34,7 @@ jobs: - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: - toolchain: 1.88 + toolchain: 1.89 components: clippy override: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0df10e6b..f66b76809 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,8 +42,8 @@ jobs: target: "aarch64-apple-darwin" platform: "arm64-mac" - name: "Windows" - runs-on: "ubuntu-22.04" - target: "x86_64-pc-windows-gnu" + runs-on: "windows-latest" + target: "x86_64-pc-windows-msvc" platform: "windows" ext: ".exe" @@ -53,6 +53,18 @@ jobs: - name: Check out code uses: actions/checkout@v4 + - name: Configure Windows long paths and Cargo homes + if: ${{ runner.os == 'Windows' }} + shell: powershell + run: | + $ErrorActionPreference = 'Stop' + git config --global core.longpaths true + New-Item -ItemType Directory -Path C:\cargo -Force | Out-Null + New-Item -ItemType Directory -Path C:\rustup -Force | Out-Null + echo "CARGO_HOME=C:\cargo" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "RUSTUP_HOME=C:\rustup" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "CARGO_NET_GIT_FETCH_WITH_CLI=true" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Cache Cargo registry uses: actions/cache@v4 with: @@ -66,6 +78,7 @@ jobs: - name: Setup prerequisites + shell: bash run: | mkdir -p dash-evo-tool/ @@ -78,7 +91,7 @@ jobs: - name: Install essentials if: ${{ runner.os == 'Linux' }} - run: sudo apt-get update && sudo apt-get install -y build-essential pkg-config clang cmake unzip libsqlite3-dev gcc-mingw-w64 mingw-w64 libsqlite3-dev mingw-w64-x86-64-dev gcc-aarch64-linux-gnu zip && uname -a && cargo clean + run: sudo apt-get update && sudo apt-get install -y build-essential pkg-config clang cmake unzip libsqlite3-dev gcc-mingw-w64 mingw-w64 libsqlite3-dev mingw-w64-x86-64-dev gcc-aarch64-linux-gnu zip nasm && uname -a && cargo clean - name: Install protoc (ARM) if: ${{ matrix.platform == 'arm64-linux' }} @@ -98,6 +111,18 @@ jobs: env: PROTOC: /usr/local/bin/protoc + - name: Install protoc (Windows MSVC) + if: ${{ matrix.target == 'x86_64-pc-windows-msvc' }} + shell: powershell + run: | + $ErrorActionPreference = 'Stop' + $url = 'https://github.com/protocolbuffers/protobuf/releases/download/v25.2/protoc-25.2-win64.zip' + Invoke-WebRequest -Uri $url -OutFile protoc.zip + Expand-Archive -Path protoc.zip -DestinationPath "$env:RUNNER_TEMP\protoc" -Force + Remove-Item protoc.zip + # Add protoc to PATH for subsequent steps + echo "$env:RUNNER_TEMP\protoc\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Install protoc (Mac x64) if: ${{ matrix.target == 'x86_64-apple-darwin' }} run: curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v25.2/protoc-25.2-osx-x86_64.zip && sudo unzip -o protoc-25.2-osx-x86_64.zip -d /usr/local bin/protoc && sudo unzip -o protoc-25.2-osx-x86_64.zip -d /usr/local 'include/*' && rm -f protoc-25.2-osx-x86_64.zip && uname -a @@ -115,18 +140,52 @@ jobs: run: curl -OL https://www.sqlite.org/2024/sqlite-dll-win-x64-3460100.zip && sudo unzip -o sqlite-dll-win-x64-3460100.zip -d winlibs && sudo chown -R runner:docker winlibs/ && pwd && ls -lah && cd winlibs && x86_64-w64-mingw32-dlltool -d sqlite3.def -l libsqlite3.a && ls -lah && cd .. - name: Build project + shell: bash run: | - cargo build --release --target ${{ matrix.target }} + set -euo pipefail + echo "::group::Cargo build output (${{ matrix.target }})" + cargo build --release --target ${{ matrix.target }} 2>&1 | tee build-${{ matrix.platform }}.log + echo "::endgroup::" mv target/${{ matrix.target }}/release/dash-evo-tool${{ matrix.ext }} dash-evo-tool/dash-evo-tool${{ matrix.ext }} env: CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar CFLAGS_x86_64_pc_windows_gnu: "-O2" + RUST_BACKTRACE: "1" + BLST_PORTABLE: ${{ matrix.target == 'x86_64-pc-windows-gnu' && '1' || '' }} - - name: Package release + - name: Summarize build errors + if: failure() + shell: bash + run: | + echo "::group::Detected error lines" + # Try to extract the most relevant error lines from the log + grep -nE "^error(\[[A-Z0-9]+\])?:|^thread '.*' panicked at|^note:|^= note:" -m 200 build-${{ matrix.platform }}.log || true + echo "::endgroup::" + echo "::group::Last 200 lines of build output" + tail -n 200 build-${{ matrix.platform }}.log || true + echo "::endgroup::" + + - name: Upload build log + if: always() + uses: actions/upload-artifact@v4 + with: + name: build-log-${{ matrix.platform }} + path: build-${{ matrix.platform }}.log + + - name: Package release (Linux/macOS) + if: ${{ runner.os != 'Windows' }} run: | zip -r dash-evo-tool-${{ matrix.platform }}.zip dash-evo-tool/ + - name: Package release (Windows) + if: ${{ runner.os == 'Windows' }} + shell: powershell + run: | + $ErrorActionPreference = 'Stop' + if (Test-Path "dash-evo-tool-${{ matrix.platform }}.zip") { Remove-Item "dash-evo-tool-${{ matrix.platform }}.zip" -Force } + Compress-Archive -Path "dash-evo-tool/*" -DestinationPath "dash-evo-tool-${{ matrix.platform }}.zip" -Force + - name: Attest uses: actions/attest-build-provenance@v1 with: @@ -177,4 +236,4 @@ jobs: ./dash-evo-tool-arm64-mac.zip ./dash-evo-tool-windows.zip draft: false - prerelease: true \ No newline at end of file + prerelease: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..383340727 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,64 @@ +name: Tests + +on: + push: + branches: + - main + - "v*-dev" + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test Suite + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-test- + ${{ runner.os }}-cargo- + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config clang cmake libsqlite3-dev + + - name: Install protoc + run: | + curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v25.2/protoc-25.2-linux-x86_64.zip + sudo unzip -o protoc-25.2-linux-x86_64.zip -d /usr/local bin/protoc + sudo unzip -o protoc-25.2-linux-x86_64.zip -d /usr/local 'include/*' + rm -f protoc-25.2-linux-x86_64.zip + env: + PROTOC: /usr/local/bin/protoc + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --workspace + + - name: Run doc tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --doc --all-features --workspace diff --git a/CLAUDE.md b/CLAUDE.md index 0bfa8aa2b..63839fe89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,6 +43,8 @@ cross build --target x86_64-pc-windows-gnu --release - **Async Backend Tasks**: Communication via crossbeam channels with result handling - **Network Isolation**: Separate app contexts per network with independent databases - **Real-time Updates**: ZMQ listeners for core blockchain events on network-specific ports +- **Custom UI components**: we build a library of reusable widgets in `ui/components` whenever we need similar + widget displayed in more than 2 places ### Critical Dependencies - **dash-sdk**: Core Dash Platform SDK (git dependency, specific revision) @@ -54,7 +56,7 @@ cross build --target x86_64-pc-windows-gnu --release ## Development Environment Setup ### Prerequisites -1. **Rust**: Version 1.88+ (enforced by rust-toolchain.toml) +1. **Rust**: Version 1.89+ (enforced by rust-toolchain.toml) 2. **System Dependencies** (Ubuntu): `build-essential libssl-dev pkg-config unzip` 3. **Protocol Buffers**: protoc v25.2+ required for dash-sdk 4. **Dash Core Wallet**: Must be synced for full functionality diff --git a/Cargo.lock b/Cargo.lock index becb99e61..8160c9277 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "ab_glyph" -version = "0.2.29" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -14,15 +14,15 @@ dependencies = [ [[package]] name = "ab_glyph_rasterizer" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "accesskit" -version = "0.17.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a" +checksum = "e25ae84c0260bdf5df07796d7cc4882460de26a2b406ec0e6c42461a723b271b" dependencies = [ "enumn", "serde", @@ -30,48 +30,37 @@ dependencies = [ [[package]] name = "accesskit_atspi_common" -version = "0.10.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c5dd55e6e94949498698daf4d48fb5659e824d7abec0d394089656ceaf99d4f" +checksum = "29bd41de2e54451a8ca0dd95ebf45b54d349d29ebceb7f20be264eee14e3d477" dependencies = [ "accesskit", - "accesskit_consumer 0.26.0", + "accesskit_consumer", "atspi-common", "serde", "thiserror 1.0.69", - "zvariant 4.2.0", -] - -[[package]] -name = "accesskit_consumer" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa3a17950ce0d911f132387777b9b3d05eddafb59b773ccaa53fceefaeb0228e" -dependencies = [ - "accesskit", - "immutable-chunkmap", + "zvariant", ] [[package]] name = "accesskit_consumer" -version = "0.26.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" +checksum = "8bfae7c152994a31dc7d99b8eeac7784a919f71d1b306f4b83217e110fd3824c" dependencies = [ "accesskit", - "hashbrown 0.15.4", - "immutable-chunkmap", + "hashbrown 0.15.5", ] [[package]] name = "accesskit_macos" -version = "0.18.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" +checksum = "692dd318ff8a7a0ffda67271c4bd10cf32249656f4e49390db0b26ca92b095f2" dependencies = [ "accesskit", - "accesskit_consumer 0.26.0", - "hashbrown 0.15.4", + "accesskit_consumer", + "hashbrown 0.15.5", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -79,42 +68,41 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.13.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcee751cc20d88678c33edaf9c07e8b693cd02819fe89053776f5313492273f5" +checksum = "c5f7474c36606d0fe4f438291d667bae7042ea2760f506650ad2366926358fc8" dependencies = [ "accesskit", "accesskit_atspi_common", - "async-channel", + "async-channel 2.5.0", "async-executor", "async-task", "atspi", "futures-lite", "futures-util", "serde", - "zbus 4.4.0", + "zbus", ] [[package]] name = "accesskit_windows" -version = "0.24.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" +checksum = "70a042b62c9c05bf7b616f015515c17d2813f3ba89978d6f4fc369735d60700a" dependencies = [ "accesskit", - "accesskit_consumer 0.26.0", - "hashbrown 0.15.4", - "paste", + "accesskit_consumer", + "hashbrown 0.15.5", "static_assertions", - "windows", - "windows-core 0.58.0", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] name = "accesskit_winit" -version = "0.23.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6a48dad5530b6deb9fc7a52cc6c3bf72cdd9eb8157ac9d32d69f2427a5e879" +checksum = "5c1f0d3d13113d8857542a4f8d1a1c24d1dc1527b77aee8426127f4901588708" dependencies = [ "accesskit", "accesskit_macos", @@ -124,15 +112,6 @@ dependencies = [ "winit", ] -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -146,7 +125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array 0.14.7", + "generic-array 0.14.9", ] [[package]] @@ -174,17 +153,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.12" @@ -192,7 +160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "serde", "version_check", @@ -221,7 +189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.9.1", + "bitflags 2.10.0", "cc", "cesu8", "jni", @@ -231,7 +199,7 @@ dependencies = [ "ndk", "ndk-context", "ndk-sys 0.6.0+11769913", - "num_enum 0.7.3", + "num_enum 0.7.5", "thiserror 1.0.69", ] @@ -241,12 +209,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -258,9 +220,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -273,9 +235,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -288,56 +250,56 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "arboard" -version = "3.5.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.1", - "objc2-app-kit 0.3.1", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "x11rb", ] @@ -359,6 +321,70 @@ dependencies = [ "password-hash", ] +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -392,6 +418,24 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3d60bee1a1d38c2077030f4788e1b4e31058d2e79a8cfc8f2b440bd44db290" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "url", + "zbus", +] + [[package]] name = "ashpd" version = "0.11.0" @@ -403,12 +447,15 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.1", + "rand 0.9.2", "raw-window-handle", "serde", "serde_repr", "url", - "zbus 5.7.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", ] [[package]] @@ -417,7 +464,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -425,9 +472,20 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -437,9 +495,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", @@ -451,9 +509,9 @@ dependencies = [ [[package]] name = "async-fs" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" dependencies = [ "async-lock", "blocking", @@ -461,31 +519,45 @@ dependencies = [ ] [[package]] -name = "async-io" +name = "async-global-executor" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", - "rustix 1.0.7", + "rustix 1.1.2", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "async-lock" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] @@ -503,21 +575,20 @@ dependencies = [ [[package]] name = "async-process" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener", + "event-listener 5.4.1", "futures-lite", - "rustix 1.0.7", - "tracing", + "rustix 1.1.2", ] [[package]] @@ -528,14 +599,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] name = "async-signal" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock", @@ -543,10 +614,36 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.0.7", + "rustix 1.1.2", "signal-hook-registry", "slab", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", ] [[package]] @@ -557,13 +654,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -587,9 +684,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" -version = "0.22.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" +checksum = "c83247582e7508838caf5f316c00791eee0e15c0bf743e6880585b867e16815c" dependencies = [ "atspi-common", "atspi-connection", @@ -598,75 +695,59 @@ dependencies = [ [[package]] name = "atspi-common" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" +checksum = "33dfc05e7cdf90988a197803bf24f5788f94f7c94a69efa95683e8ffe76cfdfb" dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus 4.4.0", + "zbus", "zbus-lockstep", "zbus-lockstep-macros", - "zbus_names 3.0.0", - "zvariant 4.2.0", + "zbus_names", + "zvariant", ] [[package]] name = "atspi-connection" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" +checksum = "4193d51303d8332304056ae0004714256b46b6635a5c556109b319c0d3784938" dependencies = [ "atspi-common", "atspi-proxies", "futures-lite", - "zbus 4.4.0", + "zbus", ] [[package]] name = "atspi-proxies" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" +checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" dependencies = [ "atspi-common", "serde", - "zbus 4.4.0", - "zvariant 4.2.0", + "zbus", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backon" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ "fastrand", "tokio", ] -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -674,16 +755,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] -name = "base64" -version = "0.13.1" +name = "base58ck" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals 0.3.0", + "bitcoin_hashes 0.14.0", +] [[package]] name = "base64" -version = "0.21.7" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" @@ -712,6 +797,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "2.0.0-rc.3" @@ -731,6 +825,24 @@ dependencies = [ "virtue 0.0.13", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.108", +] + [[package]] name = "bip37-bloom-filter" version = "0.1.0" @@ -743,15 +855,16 @@ dependencies = [ [[package]] name = "bip39" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387" +checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" dependencies = [ "bitcoin_hashes 0.13.0", "rand 0.8.5", "rand_core 0.6.4", "serde", "unicode-normalization", + "zeroize", ] [[package]] @@ -790,6 +903,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" + [[package]] name = "bitcoin-io" version = "0.1.3" @@ -802,7 +921,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ - "bitcoin-internals", + "bitcoin-internals 0.2.0", "hex-conservative 0.1.2", ] @@ -824,11 +943,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -877,7 +996,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array 0.14.9", ] [[package]] @@ -891,20 +1010,20 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.3", ] [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-task", "futures-io", "futures-lite", @@ -913,9 +1032,34 @@ dependencies = [ [[package]] name = "blsful" -version = "3.0.0-pre8" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384e5e9866cb7f830f06a6633ba998697d5a826e99e8c78376deaadd33cda7be" +checksum = "d267776bf4742935d219fcdbdf590bed0f7e5fccdf5bd168fb30b2543a0b2b24" +dependencies = [ + "anyhow", + "blstrs_plus", + "hex", + "hkdf", + "merlin", + "pairing", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", + "serde_bare", + "sha2", + "sha3", + "subtle", + "thiserror 2.0.17", + "uint-zigzag", + "vsss-rs 5.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize", +] + +[[package]] +name = "blsful" +version = "3.0.0" +source = "git+https://github.com/dashpay/agora-blsful?rev=0c34a7a488a0bd1c9a9a2196e793b303ad35c900#0c34a7a488a0bd1c9a9a2196e793b303ad35c900" dependencies = [ "anyhow", "blstrs_plus", @@ -931,9 +1075,9 @@ dependencies = [ "sha2", "sha3", "subtle", - "thiserror 2.0.12", + "thiserror 2.0.17", "uint-zigzag", - "vsss-rs", + "vsss-rs 5.1.0 (git+https://github.com/dashpay/vsss-rs?branch=main)", "zeroize", ] @@ -978,28 +1122,28 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.9.3" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -1023,13 +1167,23 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "calloop" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "log", "polling", "rustix 0.38.44", @@ -1051,10 +1205,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.26" +version = "1.2.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1067,7 +1222,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] -name = "cfg-expr" +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-expr" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" @@ -1078,9 +1242,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1099,17 +1263,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1180,21 +1343,33 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clipboard-win" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ "error-code", ] [[package]] name = "codespan-reporting" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ + "serde", "termcolor", "unicode-width", ] @@ -1205,6 +1380,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -1306,9 +1490,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1371,9 +1555,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -1381,7 +1565,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array 0.14.7", + "generic-array 0.14.9", "rand_core 0.6.4", "serdect", "subtle", @@ -1394,7 +1578,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.7", + "generic-array 0.14.9", "rand_core 0.6.4", "typenum", ] @@ -1438,51 +1622,78 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] name = "dapi-grpc" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" dependencies = [ - "dapi-grpc-macros", + "dapi-grpc-macros 2.0.1", "futures-core", "getrandom 0.2.16", - "platform-version", - "prost", + "platform-version 2.0.1", + "prost 0.13.5", "serde", "serde_bytes", "serde_json", - "tenderdash-proto", - "tonic", - "tonic-build", + "tenderdash-proto 1.4.0", + "tonic 0.13.1", + "tonic-build 0.13.1", +] + +[[package]] +name = "dapi-grpc" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "dapi-grpc-macros 2.1.0", + "futures-core", + "getrandom 0.2.16", + "platform-version 2.1.0", + "prost 0.14.1", + "serde", + "serde_bytes", + "serde_json", + "tenderdash-proto 1.5.0-dev.2", + "tonic 0.14.2", + "tonic-prost", + "tonic-prost-build", ] [[package]] name = "dapi-grpc-macros" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "heck", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dapi-grpc-macros" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ "heck", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] name = "dark-light" -version = "1.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c" +checksum = "18e1a09f280e29a8b00bc7e81eca5ac87dca0575639c9422a5fa25a07bb884b8" dependencies = [ - "dconf_rs", - "detect-desktop-environment", - "dirs 4.0.0", - "objc", - "rust-ini", + "ashpd 0.10.3", + "async-std", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "web-sys", "winreg", - "zbus 4.4.0", ] [[package]] @@ -1506,7 +1717,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -1517,28 +1728,42 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.103", + "syn 2.0.108", +] + +[[package]] +name = "dash-context-provider" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "dpp 2.1.0", + "drive 2.1.0", + "hex", + "serde", + "serde_json", + "thiserror 1.0.69", ] [[package]] name = "dash-evo-tool" -version = "0.9.2" +version = "0.9.3" dependencies = [ "aes-gcm", "arboard", "argon2", "base64 0.22.1", - "bincode", + "bincode 2.0.0-rc.3", "bip39", - "bitflags 2.9.1", + "bitflags 2.10.0", "chrono", "chrono-humanize", "crossbeam-channel", "dark-light", - "dash-sdk", + "dash-sdk 2.1.0", "derive_more 2.0.1", "directories", "dotenvy", + "ed25519-dalek", "eframe", "egui", "egui_commonmark", @@ -1547,15 +1772,18 @@ dependencies = [ "enum-iterator", "envy", "futures", + "grovestark", "hex", "humantime", "image", "itertools 0.14.0", "libsqlite3-sys", "native-dialog", - "nix 0.30.1", + "nix", "qrcode", + "rand 0.8.5", "raw-cpuid", + "rayon", "regex", "rfd", "rusqlite", @@ -1565,23 +1793,69 @@ dependencies = [ "serde_yaml", "sha2", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", "tracing-subscriber", "tz-rs", - "which", + "which 8.0.0", "zeroize", "zeromq", "zmq", "zxcvbn", ] +[[package]] +name = "dash-network" +version = "0.40.0" +source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.40.0#c877c1a74d145e2003d549619698511513db925c" +dependencies = [ + "bincode 2.0.0-rc.3", + "bincode_derive", + "hex", + "serde", +] + [[package]] name = "dash-sdk" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "arc-swap", + "async-trait", + "backon", + "bip37-bloom-filter", + "chrono", + "ciborium", + "dapi-grpc 2.0.1", + "dapi-grpc-macros 2.0.1", + "dashcore-rpc 0.39.6", + "derive_more 1.0.0", + "dotenvy", + "dpp 2.0.1", + "drive 2.0.1", + "drive-proof-verifier 2.0.1", + "envy", + "futures", + "hex", + "http", + "lru", + "rs-dapi-client 2.0.1", + "rustls-pemfile", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", + "zeroize", +] + +[[package]] +name = "dash-sdk" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ "arc-swap", "async-trait", @@ -1589,24 +1863,25 @@ dependencies = [ "bip37-bloom-filter", "chrono", "ciborium", - "dapi-grpc", - "dapi-grpc-macros", - "dashcore-rpc", + "dapi-grpc 2.1.0", + "dapi-grpc-macros 2.1.0", + "dash-context-provider", "derive_more 1.0.0", "dotenvy", - "dpp", - "drive", - "drive-proof-verifier", + "dpp 2.1.0", + "drive 2.1.0", + "drive-proof-verifier 2.1.0", "envy", "futures", "hex", "http", + "js-sys", "lru", - "rs-dapi-client", + "rs-dapi-client 2.1.0", "rustls-pemfile", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -1621,18 +1896,44 @@ dependencies = [ "anyhow", "base64-compat", "bech32", - "bitflags 2.9.1", + "bitflags 2.10.0", + "blake3", + "blsful 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "dashcore-private 0.39.6", + "dashcore_hashes 0.39.6", + "ed25519-dalek", + "hex", + "hex_lit", + "rustversion", + "secp256k1", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "dashcore" +version = "0.40.0" +source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.40.0#c877c1a74d145e2003d549619698511513db925c" +dependencies = [ + "anyhow", + "base64-compat", + "bech32", + "bincode 2.0.0-rc.3", + "bincode_derive", + "bitvec", "blake3", - "blsful", - "dashcore-private", - "dashcore_hashes", + "blsful 3.0.0 (git+https://github.com/dashpay/agora-blsful?rev=0c34a7a488a0bd1c9a9a2196e793b303ad35c900)", + "dash-network", + "dashcore-private 0.40.0", + "dashcore_hashes 0.40.0", "ed25519-dalek", "hex", "hex_lit", + "log", "rustversion", "secp256k1", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -1640,12 +1941,30 @@ name = "dashcore-private" version = "0.39.6" source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.39.6#51df58f5d5d499f5ee80ab17076ff70b5347c7db" +[[package]] +name = "dashcore-private" +version = "0.40.0" +source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.40.0#c877c1a74d145e2003d549619698511513db925c" + [[package]] name = "dashcore-rpc" version = "0.39.6" source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.39.6#51df58f5d5d499f5ee80ab17076ff70b5347c7db" dependencies = [ - "dashcore-rpc-json", + "dashcore-rpc-json 0.39.6", + "hex", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "dashcore-rpc" +version = "0.40.0" +source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.40.0#c877c1a74d145e2003d549619698511513db925c" +dependencies = [ + "dashcore-rpc-json 0.40.0", "hex", "jsonrpc", "log", @@ -1658,9 +1977,24 @@ name = "dashcore-rpc-json" version = "0.39.6" source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.39.6#51df58f5d5d499f5ee80ab17076ff70b5347c7db" dependencies = [ - "bincode", - "dashcore", + "bincode 2.0.0-rc.3", + "dashcore 0.39.6", + "hex", + "serde", + "serde_json", + "serde_repr", + "serde_with", +] + +[[package]] +name = "dashcore-rpc-json" +version = "0.40.0" +source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.40.0#c877c1a74d145e2003d549619698511513db925c" +dependencies = [ + "bincode 2.0.0-rc.3", + "dashcore 0.40.0", "hex", + "key-wallet", "serde", "serde_json", "serde_repr", @@ -1672,7 +2006,18 @@ name = "dashcore_hashes" version = "0.39.6" source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.39.6#51df58f5d5d499f5ee80ab17076ff70b5347c7db" dependencies = [ - "dashcore-private", + "dashcore-private 0.39.6", + "secp256k1", + "serde", +] + +[[package]] +name = "dashcore_hashes" +version = "0.40.0" +source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.40.0#c877c1a74d145e2003d549619698511513db925c" +dependencies = [ + "bincode 2.0.0-rc.3", + "dashcore-private 0.40.0", "secp256k1", "serde", ] @@ -1693,38 +2038,62 @@ dependencies = [ [[package]] name = "dashpay-contract" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" dependencies = [ - "platform-value", - "platform-version", + "platform-value 2.0.1", + "platform-version 2.0.1", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", +] + +[[package]] +name = "dashpay-contract" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "platform-value 2.1.0", + "platform-version 2.1.0", + "serde_json", + "thiserror 2.0.17", ] [[package]] name = "data-contracts" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" -dependencies = [ - "dashpay-contract", - "dpns-contract", - "feature-flags-contract", - "keyword-search-contract", - "masternode-reward-shares-contract", - "platform-value", - "platform-version", +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "dashpay-contract 2.0.1", + "dpns-contract 2.0.1", + "feature-flags-contract 2.0.1", + "keyword-search-contract 2.0.1", + "masternode-reward-shares-contract 2.0.1", + "platform-value 2.0.1", + "platform-version 2.0.1", "serde_json", - "thiserror 2.0.12", - "token-history-contract", - "wallet-utils-contract", - "withdrawals-contract", + "thiserror 2.0.17", + "token-history-contract 2.0.1", + "wallet-utils-contract 2.0.1", + "withdrawals-contract 2.0.1", ] [[package]] -name = "dconf_rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" +name = "data-contracts" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "dashpay-contract 2.1.0", + "dpns-contract 2.1.0", + "feature-flags-contract 2.1.0", + "keyword-search-contract 2.1.0", + "masternode-reward-shares-contract 2.1.0", + "platform-value 2.1.0", + "platform-version 2.1.0", + "serde_json", + "thiserror 2.0.17", + "token-history-contract 2.1.0", + "wallet-utils-contract 2.1.0", + "withdrawals-contract 2.1.0", +] [[package]] name = "der" @@ -1738,23 +2107,34 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -1775,7 +2155,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -1785,7 +2165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -1814,7 +2194,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", "unicode-xid", ] @@ -1826,15 +2206,9 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] -[[package]] -name = "detect-desktop-environment" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" - [[package]] name = "digest" version = "0.10.7" @@ -1863,16 +2237,7 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys 0.3.7", + "dirs-sys", ] [[package]] @@ -1881,18 +2246,7 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", + "dirs-sys", ] [[package]] @@ -1903,8 +2257,8 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.0", - "windows-sys 0.60.2", + "redox_users", + "windows-sys 0.61.2", ] [[package]] @@ -1913,28 +2267,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" -[[package]] -name = "dispatch2" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" -dependencies = [ - "bitflags 2.9.1", - "block2 0.6.1", - "libc", - "objc2 0.6.1", -] - [[package]] name = "dispatch2" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.1", - "block2 0.6.1", + "bitflags 2.10.0", + "block2 0.6.2", "libc", - "objc2 0.6.1", + "objc2 0.6.3", ] [[package]] @@ -1945,7 +2287,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -1957,17 +2299,11 @@ dependencies = [ "libloading", ] -[[package]] -name = "dlv-list" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" - [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -1993,108 +2329,212 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "dpns-contract" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "platform-value 2.0.1", + "platform-version 2.0.1", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "dpns-contract" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ - "platform-value", - "platform-version", + "platform-value 2.1.0", + "platform-version 2.1.0", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "dpp" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "bincode 2.0.0-rc.3", + "bincode_derive", + "bs58", + "byteorder", + "chrono", + "chrono-tz", + "ciborium", + "dashcore 0.39.6", + "data-contracts 2.0.1", + "derive_more 1.0.0", + "env_logger", + "getrandom 0.2.16", + "hex", + "indexmap 2.12.0", + "integer-encoding", + "itertools 0.13.0", + "lazy_static", + "nohash-hasher", + "num_enum 0.7.5", + "once_cell", + "platform-serialization 2.0.1", + "platform-serialization-derive 2.0.1", + "platform-value 2.0.1", + "platform-version 2.0.1", + "platform-versioning 2.0.1", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "serde_repr", + "sha2", + "strum 0.26.3", + "thiserror 2.0.17", +] + +[[package]] +name = "dpp" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ "anyhow", "async-trait", "base64 0.22.1", - "bincode", + "bincode 2.0.0-rc.3", "bincode_derive", "bs58", "byteorder", "chrono", "chrono-tz", "ciborium", - "dashcore", - "data-contracts", + "dashcore 0.40.0", + "dashcore-rpc 0.40.0", + "data-contracts 2.1.0", "derive_more 1.0.0", "env_logger", "getrandom 0.2.16", "hex", - "indexmap 2.9.0", + "indexmap 2.12.0", "integer-encoding", "itertools 0.13.0", + "key-wallet", "lazy_static", "nohash-hasher", - "num_enum 0.7.3", + "num_enum 0.7.5", "once_cell", - "platform-serialization", - "platform-serialization-derive", - "platform-value", - "platform-version", - "platform-versioning", + "platform-serialization 2.1.0", + "platform-serialization-derive 2.1.0", + "platform-value 2.1.0", + "platform-version 2.1.0", + "platform-versioning 2.1.0", "rand 0.8.5", "regex", "serde", "serde_json", "serde_repr", "sha2", - "strum", - "thiserror 2.0.12", + "strum 0.26.3", + "thiserror 2.0.17", + "tracing", ] [[package]] name = "drive" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" dependencies = [ - "bincode", + "bincode 2.0.0-rc.3", "byteorder", "derive_more 1.0.0", - "dpp", + "dpp 2.0.1", "grovedb", "grovedb-costs", "grovedb-epoch-based-storage-flags", "grovedb-path", "grovedb-version", "hex", - "indexmap 2.9.0", + "indexmap 2.12.0", "integer-encoding", "nohash-hasher", - "platform-version", + "platform-version 2.0.1", "serde", "sqlparser", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", ] [[package]] -name = "drive-proof-verifier" -version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +name = "drive" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ - "bincode", - "dapi-grpc", + "bincode 2.0.0-rc.3", + "byteorder", "derive_more 1.0.0", - "dpp", - "drive", - "hex", - "indexmap 2.9.0", - "platform-serialization", - "platform-serialization-derive", + "dpp 2.1.0", + "grovedb", + "grovedb-costs", + "grovedb-epoch-based-storage-flags", + "grovedb-path", + "grovedb-version", + "hex", + "indexmap 2.12.0", + "integer-encoding", + "nohash-hasher", + "platform-version 2.1.0", + "serde", + "sqlparser", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "drive-proof-verifier" +version = "2.0.1" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "bincode 2.0.0-rc.3", + "dapi-grpc 2.0.1", + "derive_more 1.0.0", + "dpp 2.0.1", + "drive 2.0.1", + "hex", + "indexmap 2.12.0", + "platform-serialization 2.0.1", + "platform-serialization-derive 2.0.1", + "serde", + "serde_json", + "tenderdash-abci 1.4.0", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "drive-proof-verifier" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "bincode 2.0.0-rc.3", + "dapi-grpc 2.1.0", + "dash-context-provider", + "derive_more 1.0.0", + "dpp 2.1.0", + "drive 2.1.0", + "hex", + "indexmap 2.12.0", + "platform-serialization 2.1.0", + "platform-serialization-derive 2.1.0", "serde", "serde_json", - "tenderdash-abci", - "thiserror 2.0.12", + "tenderdash-abci 1.5.0-dev.2", + "thiserror 2.0.17", "tracing", ] [[package]] name = "ecolor" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +checksum = "94bdf37f8d5bd9aa7f753573fdda9cf7343afa73dd28d7bfe9593bd9798fc07e" dependencies = [ "bytemuck", "emath", @@ -2129,17 +2569,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", + "serde", "signature", ] [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", + "merlin", "rand_core 0.6.4", "serde", "sha2", @@ -2149,11 +2591,11 @@ dependencies = [ [[package]] name = "eframe" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0dfe0859f3fb1bc6424c57d41e10e9093fe938f426b691e42272c2f336d915c" +checksum = "14d1c15e7bd136b309bd3487e6ffe5f668b354cd9768636a836dd738ac90eb0b" dependencies = [ - "ahash 0.8.12", + "ahash", "bytemuck", "document-features", "egui", @@ -2188,13 +2630,13 @@ dependencies = [ [[package]] name = "egui" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +checksum = "5d5d0306cd61ca75e29682926d71f2390160247f135965242e904a636f51c0dc" dependencies = [ "accesskit", - "ahash 0.8.12", - "bitflags 2.9.1", + "ahash", + "bitflags 2.10.0", "emath", "epaint", "log", @@ -2202,15 +2644,17 @@ dependencies = [ "profiling", "ron", "serde", + "smallvec", + "unicode-segmentation", ] [[package]] name = "egui-wgpu" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d319dfef570f699b6e9114e235e862a2ddcf75f0d1a061de9e1328d92146d820" +checksum = "c12eca13293f8eba27a32aaaa1c765bfbf31acd43e8d30d5881dcbe5e99ca0c7" dependencies = [ - "ahash 0.8.12", + "ahash", "bytemuck", "document-features", "egui", @@ -2226,12 +2670,12 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d9dfbb78fe4eb9c3a39ad528b90ee5915c252e77bbab9d4ebc576541ab67e13" +checksum = "f95d0a91f9cb0dc2e732d49c2d521ac8948e1f0b758f306fb7b14d6f5db3927f" dependencies = [ "accesskit_winit", - "ahash 0.8.12", + "ahash", "arboard", "bytemuck", "egui", @@ -2247,9 +2691,9 @@ dependencies = [ [[package]] name = "egui_commonmark" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1e5d9a91b1b7a320c9b7f56d1878416d7c9bab3eaf337b036e0ddfabf58623" +checksum = "26c9caff9c964af1e3d913acd85e86d2170e3169a43cf4ff84eea3106691c14d" dependencies = [ "egui", "egui_commonmark_backend", @@ -2259,9 +2703,9 @@ dependencies = [ [[package]] name = "egui_commonmark_backend" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb41b6833a6aaa99ca5c4f8e75b2410d69a7b3e30148d413f541147404a0dfa" +checksum = "6e317aa4031f27be77d4c1c33cb038cdf02d77790c28e5cf1283a66cceb88695" dependencies = [ "egui", "egui_extras", @@ -2270,11 +2714,11 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624659a2e972a46f4d5f646557906c55f1cd5a0836eddbe610fdf1afba1b4226" +checksum = "dddbceddf39805fc6c62b1f7f9c05e23590b40844dc9ed89c6dc6dbc886e3e3b" dependencies = [ - "ahash 0.8.12", + "ahash", "egui", "enum-map", "image", @@ -2285,11 +2729,11 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" +checksum = "cc7037813341727937f9e22f78d912f3e29bc3c46e2f40a9e82bb51cbf5e4cfb" dependencies = [ - "ahash 0.8.12", + "ahash", "bytemuck", "egui", "glow", @@ -2303,9 +2747,9 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c46def610cf9486675aeec698d4e36a949ec0b0e1f6096135b0584dcfd52aa47" +checksum = "5bb00f16e00af09092c117515246732adba4ca4649463bdbc2ab6114a2944765" dependencies = [ "eframe", "egui", @@ -2328,7 +2772,7 @@ dependencies = [ "crypto-bigint", "digest", "ff", - "generic-array 0.14.7", + "generic-array 0.14.9", "group", "hkdf", "pkcs8", @@ -2342,8 +2786,21 @@ dependencies = [ [[package]] name = "elliptic-curve-tools" version = "0.1.2" +source = "git+https://github.com/mikelodder7/elliptic-curve-tools?rev=c989865fa71503d2cbf5c5795c4ebcf4a2f3221c#c989865fa71503d2cbf5c5795c4ebcf4a2f3221c" +dependencies = [ + "elliptic-curve", + "heapless", + "hex", + "multiexp", + "serde", + "zeroize", +] + +[[package]] +name = "elliptic-curve-tools" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48843edfbd0a370b3dd14cdbb4e446e9a8855311e6b2b57bf9a1fd1367bc317" +checksum = "1de2b6fae800f08032a6ea32995b52925b1d451bff9d445c8ab2932323277faf" dependencies = [ "elliptic-curve", "heapless", @@ -2355,9 +2812,9 @@ dependencies = [ [[package]] name = "emath" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" +checksum = "45fd7bc25f769a3c198fe1cf183124bf4de3bd62ef7b4f1eaf6b08711a3af8db" dependencies = [ "bytemuck", "serde", @@ -2380,22 +2837,22 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enum-iterator" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" dependencies = [ "enum-iterator-derive", ] [[package]] name = "enum-iterator-derive" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -2405,7 +2862,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" dependencies = [ "enum-map-derive", - "serde", ] [[package]] @@ -2416,7 +2872,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -2437,7 +2893,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -2448,14 +2904,14 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -2491,12 +2947,12 @@ dependencies = [ [[package]] name = "epaint" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +checksum = "63adcea970b7a13094fe97a36ab9307c35a750f9e24bf00bb7ef3de573e0fddb" dependencies = [ "ab_glyph", - "ahash 0.8.12", + "ahash", "bytemuck", "ecolor", "emath", @@ -2510,9 +2966,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" +checksum = "1537accc50c9cab5a272c39300bdd0dd5dca210f6e5e8d70be048df9596e7ca2" [[package]] name = "equivalent" @@ -2522,12 +2978,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2538,9 +2994,15 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "event-listener" -version = "5.4.0" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -2553,7 +3015,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener", + "event-listener 5.4.1", "pin-project-lite", ] @@ -2576,8 +3038,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" dependencies = [ "bit-set 0.5.3", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -2586,6 +3048,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -2598,12 +3080,23 @@ dependencies = [ [[package]] name = "feature-flags-contract" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "platform-value 2.0.1", + "platform-version 2.0.1", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "feature-flags-contract" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ - "platform-value", - "platform-version", + "platform-value 2.1.0", + "platform-version 2.1.0", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -2623,6 +3116,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "fixedbitset" version = "0.5.7" @@ -2631,11 +3130,12 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -2660,6 +3160,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -2687,7 +3193,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -2704,9 +3210,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2779,9 +3285,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -2798,7 +3304,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -2833,9 +3339,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -2844,22 +3350,23 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.2.0" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c8444bc9d71b935156cc0ccab7f622180808af7867b1daae6547d773591703" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" dependencies = [ - "serde", + "rustversion", + "serde_core", "typenum", ] [[package]] name = "gethostname" -version = "0.4.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "libc", - "windows-targets 0.48.5", + "rustix 1.1.2", + "windows-link 0.2.1", ] [[package]] @@ -2871,20 +3378,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", ] [[package]] @@ -2897,12 +3404,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "gl_generator" version = "0.14.0" @@ -2916,9 +3417,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "gloo-timers" @@ -2950,18 +3451,18 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cfg_aliases", "cgl", - "dispatch2 0.3.0", + "dispatch2", "glutin_egl_sys", "glutin_glx_sys", "glutin_wgl_sys", "libloading", - "objc2 0.6.1", - "objc2-app-kit 0.3.1", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", "raw-window-handle", "wayland-sys", @@ -3016,7 +3517,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "gpu-alloc-types", ] @@ -3026,7 +3527,19 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", ] [[package]] @@ -3035,9 +3548,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "gpu-descriptor-types", - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -3046,7 +3559,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", ] [[package]] @@ -3064,105 +3577,172 @@ dependencies = [ [[package]] name = "grovedb" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611077565b279965fa34897787ae52f79471f0476db785116cceb92077f237ad" +checksum = "f12b2378c5eda5b7cadceb34fc6e0a8fd87fe03fc04841a7d32a74ff73ccef71" dependencies = [ - "bincode", + "bincode 2.0.0-rc.3", "bincode_derive", "blake3", "grovedb-costs", "grovedb-merk", "grovedb-path", + "grovedb-storage", "grovedb-version", + "grovedb-visualize", "hex", "hex-literal", - "indexmap 2.9.0", + "indexmap 2.12.0", "integer-encoding", + "intmap", + "itertools 0.14.0", "reqwest", "sha2", - "thiserror 2.0.12", + "tempfile", + "thiserror 2.0.17", ] [[package]] name = "grovedb-costs" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab159c3f82b0387f6a27a54930b18aa594b507013de947c8e909cf61abb75fe" +checksum = "e74fafe53bf5ae27128799856e557ef5cb2d7109f1f7bc7f4440bbd0f97c7072" dependencies = [ "integer-encoding", "intmap", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "grovedb-epoch-based-storage-flags" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce2f34c6bfddb3a26696b42e6169f986330513e0e9f4c5d7ba290d09867a5e" +checksum = "bc6bdc033cc229b17cd02ee9d5c5a5a344788ed0e69ad7468b0d34d94b021fc4" dependencies = [ "grovedb-costs", "hex", "integer-encoding", "intmap", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "grovedb-merk" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4580e54da0031d2f36e50312f3361005099bceeb8adb0f6ccbf87a0880cd1b08" +checksum = "b6dd6f733e9d5c15c98e05b68a2028e00b7f177baa51e8d8c1541102942a72b7" dependencies = [ - "bincode", + "bincode 2.0.0-rc.3", "bincode_derive", "blake3", "byteorder", + "colored", "ed", "grovedb-costs", "grovedb-path", + "grovedb-storage", "grovedb-version", "grovedb-visualize", "hex", - "indexmap 2.9.0", + "indexmap 2.12.0", "integer-encoding", - "thiserror 2.0.12", + "num_cpus", + "rand 0.8.5", + "thiserror 2.0.17", ] [[package]] name = "grovedb-path" -version = "3.0.0" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01f716520d6c6b0f25dc4a68bc7dded645826ed57d38a06a80716a487c09d23c" +dependencies = [ + "hex", +] + +[[package]] +name = "grovedb-storage" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d61e09bb3055358974ceb65b91752064979450092014d91a6bc4a52d77887ea" +checksum = "52d04f3831fe210543a7246f2a60ae068f23eac5f9d53200d5a82785750f68fd" dependencies = [ + "blake3", + "grovedb-costs", + "grovedb-path", + "grovedb-visualize", "hex", + "integer-encoding", + "lazy_static", + "num_cpus", + "rocksdb", + "strum 0.27.2", + "tempfile", + "thiserror 2.0.17", ] [[package]] name = "grovedb-version" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d61d27c76d49758b365a9e4a9da7f995f976b9525626bf645aef258024defd2" +checksum = "cdc855662f05f41b10dd022226cb78e345a33f35c390e25338d21dedd45966ae" dependencies = [ - "thiserror 2.0.12", + "thiserror 2.0.17", "versioned-feature-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "grovedb-visualize" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaebfe3c1e5f263f14fd25ab060543b31eb4b9d6bdc44fe220e88df6be7ddf59" +checksum = "34fa6f41c110d1d141bf912175f187ef51ac5d2a8f163dfd229be007461a548f" dependencies = [ "hex", "itertools 0.14.0", ] +[[package]] +name = "grovestark" +version = "0.1.0" +source = "git+https://www.github.com/pauldelucia/grovestark?rev=5313ba9df590f114e11934e281f1e8c8bc462794#5313ba9df590f114e11934e281f1e8c8bc462794" +dependencies = [ + "ark-ff", + "base64 0.22.1", + "bincode 1.3.3", + "bincode 2.0.0-rc.3", + "blake3", + "bs58", + "curve25519-dalek", + "dash-sdk 2.0.1", + "ed25519-dalek", + "env_logger", + "grovedb", + "grovedb-costs", + "grovedb-merk", + "hex", + "log", + "num-bigint", + "num-traits", + "num_cpus", + "once_cell", + "rand 0.8.5", + "rayon", + "serde", + "serde_json", + "sha2", + "subtle", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "winterfell", + "zeroize", +] + [[package]] name = "h2" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -3170,7 +3750,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -3179,12 +3759,14 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "num-traits", + "zerocopy", ] [[package]] @@ -3201,29 +3783,31 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "ahash 0.8.12", "allocator-api2", + "equivalent", + "foldhash 0.1.5", ] [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -3232,7 +3816,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -3319,11 +3903,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3384,19 +3968,20 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -3404,6 +3989,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -3456,9 +4042,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", @@ -3472,7 +4058,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.1", "system-configuration", "tokio", "tower-service", @@ -3482,9 +4068,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3492,7 +4078,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -3598,9 +4184,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3619,26 +4205,18 @@ dependencies = [ [[package]] name = "image" -version = "0.25.6" +version = "0.25.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" dependencies = [ "bytemuck", "byteorder-lite", + "moxcms", "num-traits", "png", "tiff", ] -[[package]] -name = "immutable-chunkmap" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578" -dependencies = [ - "arrayvec", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -3652,13 +4230,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.0", "serde", + "serde_core", ] [[package]] @@ -3667,7 +4246,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array 0.14.7", + "generic-array 0.14.9", ] [[package]] @@ -3678,9 +4257,9 @@ checksum = "0d762194228a2f1c11063e46e32e5acb96e66e906382b9eb5441f2e0504bbd5a" [[package]] name = "intmap" -version = "3.1.1" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6958acfd72ba79d943b048ab4064c671018b6a348a715b5b8931baf975439553" +checksum = "a2e611826a1868311677fdcdfbec9e8621d104c732d080f546a854530232f0ee" dependencies = [ "serde", ] @@ -3703,9 +4282,18 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] [[package]] name = "itertools" @@ -3733,9 +4321,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -3746,13 +4334,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -3779,25 +4367,19 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" - [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -3834,15 +4416,50 @@ dependencies = [ ] [[package]] -name = "keyword-search-contract" -version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +name = "key-wallet" +version = "0.40.0" +source = "git+https://github.com/dashpay/rust-dashcore?tag=v0.40.0#c877c1a74d145e2003d549619698511513db925c" dependencies = [ - "platform-value", - "platform-version", + "base58ck", + "bip39", + "bitflags 2.10.0", + "dash-network", + "dashcore 0.40.0", + "dashcore-private 0.40.0", + "dashcore_hashes 0.40.0", + "getrandom 0.2.16", + "hex", + "hkdf", + "rand 0.8.5", + "secp256k1", + "serde", "serde_json", - "thiserror 2.0.12", -] + "sha2", + "tracing", + "zeroize", +] + +[[package]] +name = "keyword-search-contract" +version = "2.0.1" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "platform-value 2.0.1", + "platform-version 2.0.1", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "keyword-search-contract" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "platform-value 2.1.0", + "platform-version 2.1.0", + "serde_json", + "thiserror 2.0.17", +] [[package]] name = "khronos-egl" @@ -3863,15 +4480,24 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f659954571a3c132356bd15c25f0dcf14d270a28ec5c58797adc2f432831bed5" +checksum = "7c1bfc4cb16136b6f00fb85a281e4b53d026401cf5dff9a427c466bde5891f0b" dependencies = [ "accesskit", - "accesskit_consumer 0.25.0", + "accesskit_consumer", "parking_lot", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -3886,36 +4512,77 @@ checksum = "744a4c881f502e98c2241d2e5f50040ac73b30194d64452bb6260393b53f0dc9" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-link 0.2.1", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.5.18", +] + +[[package]] +name = "librocksdb-sys" +version = "0.17.3+10.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "cef2a00ee60fe526157c9023edab23943fae1ce2ab6f4abb2a807c1746835de9" dependencies = [ - "bitflags 2.9.1", + "bindgen", + "bzip2-sys", + "cc", "libc", - "redox_syscall 0.5.13", + "libz-sys", + "lz4-sys", + "zstd-sys", ] [[package]] name = "libsqlite3-sys" -version = "0.34.0" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "libz-sys" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91632f3b4fb6bd1d72aa3d78f41ffecfcf2b1a6648d8c241dbe7dbfaf4875e15" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" dependencies = [ "cc", "pkg-config", @@ -3930,9 +4597,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -3942,25 +4609,27 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" -version = "0.4.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +dependencies = [ + "value-bag", +] [[package]] name = "lru" @@ -3968,7 +4637,17 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", ] [[package]] @@ -3983,34 +4662,45 @@ dependencies = [ [[package]] name = "masternode-reward-shares-contract" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "platform-value 2.0.1", + "platform-version 2.0.1", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "masternode-reward-shares-contract" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ - "platform-value", - "platform-version", + "platform-value 2.1.0", + "platform-version 2.1.0", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.5" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -4042,7 +4732,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -4069,6 +4759,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -4081,23 +4777,34 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692af879e4d9383c0fd9dec15524af6b6977c8bf1c6b278a4526d5341347c574" +dependencies = [ + "num-traits", + "pxfm", ] [[package]] name = "multiexp" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a383da1ae933078ddb1e4141f1dd617b512b4183779d6977e6451b0e644806" +checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" dependencies = [ "ff", "group", + "rand_core 0.6.4", "rustversion", "std-shims", "zeroize", @@ -4117,47 +4824,50 @@ checksum = "9252111cf132ba0929b6f8e030cac2a24b507f3a4d6db6fb2896f27b354c714b" [[package]] name = "naga" -version = "24.0.0" +version = "25.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set 0.8.0", - "bitflags 2.9.1", + "bitflags 2.10.0", "cfg_aliases", "codespan-reporting", + "half", + "hashbrown 0.15.5", "hexf-parse", - "indexmap 2.9.0", + "indexmap 2.12.0", "log", + "num-traits", + "once_cell", "rustc-hash 1.1.0", "spirv", - "strum", - "termcolor", - "thiserror 2.0.12", - "unicode-xid", + "strum 0.26.3", + "thiserror 2.0.17", + "unicode-ident", ] [[package]] name = "native-dialog" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f006431cea71a83e6668378cb5abc2d52af299cbac6dca1780c6eeca90822df" +checksum = "1657b63bf0e60ee0eca886b5df70269240b6197b6ee46ec37da9a7d28d8e8e24" dependencies = [ "ascii", - "block2 0.6.1", - "dirs 6.0.0", - "dispatch2 0.3.0", + "block2 0.6.2", + "dirs", + "dispatch2", "formatx", - "objc2 0.6.1", - "objc2-app-kit 0.3.1", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "raw-window-handle", - "thiserror 2.0.12", + "thiserror 2.0.17", "versions", "wfd", - "which", + "which 7.0.3", "winapi", ] @@ -4184,11 +4894,11 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", - "num_enum 0.7.3", + "num_enum 0.7.5", "raw-window-handle", "thiserror 1.0.69", ] @@ -4217,26 +4927,13 @@ dependencies = [ "jni-sys", ] -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -4249,6 +4946,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -4260,12 +4967,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -4319,7 +5025,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -4361,6 +5067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4384,11 +5091,12 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ - "num_enum_derive 0.7.3", + "num_enum_derive 0.7.5", + "rustversion", ] [[package]] @@ -4405,14 +5113,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -4442,9 +5150,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] @@ -4455,33 +5163,28 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "block2 0.5.1", "libc", "objc2 0.5.2", - "objc2-core-data 0.2.2", - "objc2-core-image 0.2.2", + "objc2-core-data", + "objc2-core-image", "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "objc2-quartz-core", ] [[package]] name = "objc2-app-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.1", - "block2 0.6.1", - "libc", - "objc2 0.6.1", - "objc2-cloud-kit 0.3.1", - "objc2-core-data 0.3.1", + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", "objc2-core-graphics", - "objc2-core-image 0.3.1", - "objc2-foundation 0.3.1", - "objc2-quartz-core 0.3.1", + "objc2-foundation 0.3.2", ] [[package]] @@ -4490,24 +5193,13 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-cloud-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" -dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", - "objc2-foundation 0.3.1", -] - [[package]] name = "objc2-contacts" version = "0.2.2" @@ -4525,50 +5217,34 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-core-data" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" -dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", - "objc2-foundation 0.3.1", -] - [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.1", - "block2 0.6.1", - "dispatch2 0.3.0", - "libc", - "objc2 0.6.1", + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", ] [[package]] name = "objc2-core-graphics" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.1", - "block2 0.6.1", - "dispatch2 0.3.0", - "libc", - "objc2 0.6.1", + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", "objc2-core-foundation", "objc2-io-surface", - "objc2-metal 0.3.1", ] [[package]] @@ -4580,17 +5256,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal 0.2.2", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" -dependencies = [ - "objc2 0.6.1", - "objc2-foundation 0.3.1", + "objc2-metal", ] [[package]] @@ -4617,7 +5283,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "block2 0.5.1", "dispatch", "libc", @@ -4626,25 +5292,23 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.1", - "block2 0.6.1", - "libc", - "objc2 0.6.1", + "bitflags 2.10.0", + "objc2 0.6.3", "objc2-core-foundation", ] [[package]] name = "objc2-io-surface" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "bitflags 2.10.0", + "objc2 0.6.3", "objc2-core-foundation", ] @@ -4666,45 +5330,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-metal" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874" -dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", - "objc2-foundation 0.3.1", -] - [[package]] name = "objc2-quartz-core" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" -dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", - "objc2-foundation 0.3.1", + "objc2-metal", ] [[package]] @@ -4723,16 +5365,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", - "objc2-cloud-kit 0.2.2", - "objc2-core-data 0.2.2", - "objc2-core-image 0.2.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core 0.2.2", + "objc2-quartz-core", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -4755,22 +5397,13 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -4779,9 +5412,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -4791,11 +5424,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -4812,7 +5445,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -4823,9 +5456,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -4857,16 +5490,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-multimap" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" -dependencies = [ - "dlv-list", - "hashbrown 0.12.3", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -4877,17 +5500,11 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "owned_ttf_parser" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ "ttf-parser", ] @@ -4909,9 +5526,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -4919,15 +5536,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.13", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -4958,9 +5575,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" @@ -4969,7 +5586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.9.0", + "indexmap 2.12.0", ] [[package]] @@ -5012,7 +5629,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", "unicase", ] @@ -5043,7 +5660,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -5088,72 +5705,134 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "platform-serialization" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "bincode 2.0.0-rc.3", + "platform-version 2.0.1", +] + +[[package]] +name = "platform-serialization" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ - "bincode", - "platform-version", + "bincode 2.0.0-rc.3", + "platform-version 2.1.0", ] [[package]] name = "platform-serialization-derive" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", + "virtue 0.0.17", +] + +[[package]] +name = "platform-serialization-derive" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", "virtue 0.0.17", ] [[package]] name = "platform-value" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "base64 0.22.1", + "bincode 2.0.0-rc.3", + "bs58", + "ciborium", + "hex", + "indexmap 2.12.0", + "platform-serialization 2.0.1", + "platform-version 2.0.1", + "rand 0.8.5", + "serde", + "serde_json", + "thiserror 2.0.17", + "treediff", +] + +[[package]] +name = "platform-value" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ "base64 0.22.1", - "bincode", + "bincode 2.0.0-rc.3", "bs58", "ciborium", "hex", - "indexmap 2.9.0", - "platform-serialization", - "platform-version", + "indexmap 2.12.0", + "platform-serialization 2.1.0", + "platform-version 2.1.0", "rand 0.8.5", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "treediff", ] [[package]] name = "platform-version" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "bincode 2.0.0-rc.3", + "grovedb-version", + "once_cell", + "thiserror 2.0.17", + "versioned-feature-core 1.0.0 (git+https://github.com/dashpay/versioned-feature-core)", +] + +[[package]] +name = "platform-version" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ - "bincode", + "bincode 2.0.0-rc.3", "grovedb-version", "once_cell", - "thiserror 2.0.12", + "thiserror 2.0.17", "versioned-feature-core 1.0.0 (git+https://github.com/dashpay/versioned-feature-core)", ] [[package]] name = "platform-versioning" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", +] + +[[package]] +name = "platform-versioning" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] name = "png" -version = "0.17.16" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -5162,17 +5841,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.8.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.0.7", - "tracing", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] @@ -5210,9 +5888,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -5233,13 +5911,19 @@ dependencies = [ ] [[package]] -name = "prettyplease" -version = "0.2.34" +name = "presser" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -5254,27 +5938,27 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.22.27", + "toml_edit 0.23.7", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" [[package]] name = "prost" @@ -5283,7 +5967,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive 0.14.1", ] [[package]] @@ -5299,10 +5993,32 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost", - "prost-types", + "prost 0.13.5", + "prost-types 0.13.5", + "regex", + "syn 2.0.108", + "tempfile", +] + +[[package]] +name = "prost-build" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.14.1", + "prost-types 0.14.1", + "pulldown-cmark", + "pulldown-cmark-to-cmark", "regex", - "syn 2.0.103", + "syn 2.0.108", "tempfile", ] @@ -5316,7 +6032,20 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] @@ -5325,20 +6054,47 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ - "prost", + "prost 0.13.5", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost 0.14.1", ] [[package]] name = "pulldown-cmark" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "memchr", "unicase", ] +[[package]] +name = "pulldown-cmark-to-cmark" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5b6a0769a491a08b31ea5c62494a8f144ee0987d86d670a8af4df1e1b7cde75" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "pxfm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +dependencies = [ + "num-traits", +] + [[package]] name = "qrcode" version = "0.14.1" @@ -5348,11 +6104,17 @@ dependencies = [ "image", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", "serde", @@ -5369,18 +6131,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -5401,9 +6163,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -5444,7 +6206,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -5456,13 +6218,19 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "range-alloc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" + [[package]] name = "raw-cpuid" -version = "11.5.0" +version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", ] [[package]] @@ -5473,9 +6241,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -5483,9 +6251,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -5502,78 +6270,52 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "redox_users" -version = "0.4.6" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", + "bitflags 2.10.0", ] [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "renderdoc-sys" @@ -5583,9 +6325,9 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", @@ -5625,19 +6367,19 @@ dependencies = [ [[package]] name = "rfd" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ - "ashpd", - "block2 0.6.1", - "dispatch2 0.2.0", + "ashpd 0.11.0", + "block2 0.6.2", + "dispatch2", "js-sys", "log", - "objc2 0.6.1", - "objc2-app-kit 0.3.1", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "pollster", "raw-window-handle", "urlencoding", @@ -5661,51 +6403,90 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rocksdb" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb7af00d2b17dbd07d82c0063e25411959748ff03e8d4f96134c2ff41fce34f" +dependencies = [ + "libc", + "librocksdb-sys", +] + [[package]] name = "ron" -version = "0.8.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" dependencies = [ - "base64 0.21.7", - "bitflags 2.9.1", + "base64 0.22.1", + "bitflags 2.10.0", "serde", "serde_derive", + "unicode-ident", ] [[package]] name = "rs-dapi-client" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "backon", + "chrono", + "dapi-grpc 2.0.1", + "futures", + "getrandom 0.2.16", + "gloo-timers", + "hex", + "http", + "http-serde", + "lru", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.17", + "tokio", + "tonic-web-wasm-client 0.7.1", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "rs-dapi-client" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ "backon", "chrono", - "dapi-grpc", + "dapi-grpc 2.1.0", "futures", "getrandom 0.2.16", "gloo-timers", "hex", "http", + "http-body-util", "http-serde", "lru", "rand 0.8.5", "serde", "serde_json", "sha2", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", - "tonic-web-wasm-client", + "tonic-web-wasm-client 0.8.0", + "tower-service", "tracing", "wasm-bindgen-futures", ] [[package]] name = "rusqlite" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de23c3319433716cf134eed225fe9986bc24f63bed9be9f20c329029e672dc7" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -5715,9 +6496,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -5726,43 +6507,27 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.103", + "syn 2.0.108", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185" dependencies = [ "sha2", "walkdir", ] -[[package]] -name = "rust-ini" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -5790,7 +6555,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -5799,22 +6564,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ "log", "once_cell", @@ -5827,14 +6592,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.5.1", ] [[package]] @@ -5857,9 +6622,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -5868,9 +6633,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -5889,11 +6654,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5929,7 +6694,7 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", - "generic-array 0.14.7", + "generic-array 0.14.9", "pkcs8", "subtle", "zeroize", @@ -5962,7 +6727,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -5971,11 +6736,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5984,9 +6749,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -5994,16 +6759,17 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -6018,35 +6784,46 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.12.0", "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -6057,7 +6834,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -6106,7 +6883,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -6115,7 +6892,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.12.0", "itoa", "ryu", "serde", @@ -6132,17 +6909,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.9" @@ -6181,9 +6947,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -6211,12 +6977,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slotmap" @@ -6239,7 +7002,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -6289,21 +7052,28 @@ dependencies = [ ] [[package]] -name = "spin" -version = "0.9.8" +name = "socket2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ - "lock_api", + "libc", + "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", ] [[package]] @@ -6327,9 +7097,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -6339,11 +7109,12 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "std-shims" -version = "0.1.1" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e49360f31b0b75a6a82a5205c6103ea07a79a60808d44f5cc879d303337926" +checksum = "227c4f8561598188d0df96dbe749824576174bba278b5b6bb2eacff1066067d0" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.16.0", + "rustversion", "spin", ] @@ -6365,7 +7136,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -6378,7 +7158,19 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.103", + "syn 2.0.108", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] @@ -6409,9 +7201,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.103" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -6435,7 +7227,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -6444,7 +7236,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6486,15 +7278,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.7", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] @@ -6506,8 +7298,23 @@ dependencies = [ "hex", "lhash", "semver", - "tenderdash-proto", - "thiserror 2.0.12", + "tenderdash-proto 1.4.0", + "thiserror 2.0.17", + "tracing", + "url", +] + +[[package]] +name = "tenderdash-abci" +version = "1.5.0-dev.2" +source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.5.0-dev.2#3f6ac716c42125a01caceb42cc5997efa41c88fc" +dependencies = [ + "bytes", + "hex", + "lhash", + "semver", + "tenderdash-proto 1.5.0-dev.2", + "thiserror 2.0.17", "tracing", "url", ] @@ -6523,10 +7330,28 @@ dependencies = [ "flex-error", "num-derive", "num-traits", - "prost", + "prost 0.13.5", + "serde", + "subtle-encoding", + "tenderdash-proto-compiler 1.4.0", + "time", +] + +[[package]] +name = "tenderdash-proto" +version = "1.5.0-dev.2" +source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.5.0-dev.2#3f6ac716c42125a01caceb42cc5997efa41c88fc" +dependencies = [ + "bytes", + "chrono", + "derive_more 2.0.1", + "num-derive", + "num-traits", + "prost 0.14.1", "serde", "subtle-encoding", - "tenderdash-proto-compiler", + "tenderdash-proto-compiler 1.5.0-dev.2", + "thiserror 2.0.17", "time", ] @@ -6536,12 +7361,26 @@ version = "1.4.0" source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.4.0#e2dd15f39246081e7d569e585ab78ff5340116ac" dependencies = [ "fs_extra", - "prost-build", + "prost-build 0.13.5", + "regex", + "tempfile", + "ureq", + "walkdir", + "zip 2.4.2", +] + +[[package]] +name = "tenderdash-proto-compiler" +version = "1.5.0-dev.2" +source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.5.0-dev.2#3f6ac716c42125a01caceb42cc5997efa41c88fc" +dependencies = [ + "fs_extra", + "prost-build 0.14.1", "regex", "tempfile", "ureq", "walkdir", - "zip", + "zip 5.1.1", ] [[package]] @@ -6564,11 +7403,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -6579,18 +7418,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -6613,20 +7452,23 @@ dependencies = [ [[package]] name = "tiff" -version = "0.9.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" dependencies = [ + "fax", "flate2", - "jpeg-decoder", + "half", + "quick-error", "weezl", + "zune-jpeg", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -6639,15 +7481,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -6690,9 +7532,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -6706,41 +7548,51 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "token-history-contract" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" +dependencies = [ + "platform-value 2.0.1", + "platform-version 2.0.1", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "token-history-contract" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" dependencies = [ - "platform-value", - "platform-version", + "platform-value 2.1.0", + "platform-version 2.1.0", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "tokio" -version = "1.45.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -6755,9 +7607,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -6776,9 +7628,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -6796,7 +7648,7 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_edit 0.22.27", ] @@ -6809,14 +7661,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.9.0", - "toml_datetime", + "indexmap 2.12.0", + "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -6826,11 +7687,32 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.12.0", "serde", "serde_spanned", - "toml_datetime", - "winnow 0.7.11", + "toml_datetime 0.6.11", + "winnow 0.7.13", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", ] [[package]] @@ -6851,9 +7733,9 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "prost 0.13.5", "rustls-native-certs", - "socket2", + "socket2 0.5.10", "tokio", "tokio-rustls", "tokio-stream", @@ -6864,6 +7746,37 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "rustls-native-certs", + "socket2 0.6.1", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", + "webpki-roots 1.0.3", +] + [[package]] name = "tonic-build" version = "0.13.1" @@ -6872,10 +7785,49 @@ checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847" dependencies = [ "prettyplease", "proc-macro2", - "prost-build", - "prost-types", + "prost-build 0.13.5", + "prost-types 0.13.5", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tonic-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost 0.14.1", + "tonic 0.14.2", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build 0.14.1", + "prost-types 0.14.1", + "quote", + "syn 2.0.108", + "tempfile", + "tonic-build 0.14.2", ] [[package]] @@ -6894,8 +7846,33 @@ dependencies = [ "httparse", "js-sys", "pin-project", - "thiserror 2.0.12", - "tonic", + "thiserror 2.0.17", + "tonic 0.13.1", + "tower-service", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "tonic-web-wasm-client" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898cd44be5e23e59d2956056538f1d6b3c5336629d384ffd2d92e76f87fb98ff" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "httparse", + "js-sys", + "pin-project", + "thiserror 2.0.17", + "tonic 0.14.2", "tower-service", "wasm-bindgen", "wasm-bindgen-futures", @@ -6911,7 +7888,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.9.0", + "indexmap 2.12.0", "pin-project-lite", "slab", "sync_wrapper", @@ -6928,7 +7905,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -6965,13 +7942,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -6997,14 +7974,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -7042,15 +8019,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "tz-rs" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1450bf2b99397e72070e7935c89facaa80092ac812502200375f1f7d33c71a1" +checksum = "14eff19b8dc1ace5bf7e4d920b2628ae3837f422ff42210cb1567cbf68b5accf" [[package]] name = "uds_windows" @@ -7080,15 +8057,15 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -7101,9 +8078,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -7135,9 +8112,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.0.11" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" dependencies = [ "base64 0.22.1", "flate2", @@ -7148,14 +8125,14 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf-8", - "webpki-roots 0.26.11", + "webpki-roots 1.0.3", ] [[package]] name = "ureq-proto" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" dependencies = [ "base64 0.22.1", "http", @@ -7165,9 +8142,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -7201,12 +8178,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", + "serde", "wasm-bindgen", ] @@ -7216,6 +8194,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + [[package]] name = "vcpkg" version = "0.2.15" @@ -7252,7 +8236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80a7e511ce1795821207a837b7b1c8d8aca0c648810966ad200446ae58f6667f" dependencies = [ "itertools 0.14.0", - "nom", + "nom 8.0.0", ] [[package]] @@ -7275,8 +8259,26 @@ checksum = "fec4ebcc5594130c31b49594d55c0583fe80621f252f570b222ca4845cafd3cf" dependencies = [ "crypto-bigint", "elliptic-curve", - "elliptic-curve-tools", - "generic-array 1.2.0", + "elliptic-curve-tools 0.1.2", + "generic-array 1.3.5", + "hex", + "num", + "rand_core 0.6.4", + "serde", + "sha3", + "subtle", + "zeroize", +] + +[[package]] +name = "vsss-rs" +version = "5.1.0" +source = "git+https://github.com/dashpay/vsss-rs?branch=main#668f1406bf25a4b9a95cd97c9069f7a1632897c3" +dependencies = [ + "crypto-bigint", + "elliptic-curve", + "elliptic-curve-tools 0.2.0", + "generic-array 1.3.5", "hex", "num", "rand_core 0.6.4", @@ -7299,12 +8301,23 @@ dependencies = [ [[package]] name = "wallet-utils-contract" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" dependencies = [ - "platform-value", - "platform-version", + "platform-value 2.0.1", + "platform-version 2.0.1", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", +] + +[[package]] +name = "wallet-utils-contract" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "platform-value 2.1.0", + "platform-version 2.1.0", + "serde_json", + "thiserror 2.0.17", ] [[package]] @@ -7323,45 +8336,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -7372,9 +8386,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7382,22 +8396,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -7417,13 +8431,13 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.44", + "rustix 1.1.2", "scoped-tls", "smallvec", "wayland-sys", @@ -7431,12 +8445,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.1", - "rustix 0.38.44", + "bitflags 2.10.0", + "rustix 1.1.2", "wayland-backend", "wayland-scanner", ] @@ -7447,29 +8461,29 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182" +checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" dependencies = [ - "rustix 0.38.44", + "rustix 1.1.2", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.8" +version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7477,11 +8491,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" +checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7490,11 +8504,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7503,9 +8517,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", "quick-xml 0.37.5", @@ -7514,9 +8528,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" dependencies = [ "dlib", "log", @@ -7526,9 +8540,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -7546,17 +8560,16 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5df295f8451142f1856b1bd86a606dfe9587d439bc036e319c827700dbd555e" +checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" dependencies = [ "core-foundation 0.10.1", - "home", "jni", "log", "ndk-context", - "objc2 0.6.1", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", "url", "web-sys", ] @@ -7567,14 +8580,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.0", + "webpki-roots 1.0.3", ] [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] @@ -7587,9 +8600,9 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "wfd" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e713040b67aae5bf1a0ae3e1ebba8cc29ab2b90da9aa1bff6e09031a8a41d7a8" +checksum = "0c17bbfb155305bcb79144f568c3b796275ba4db5d5856597bc85acefe29b819" dependencies = [ "libc", "winapi", @@ -7597,17 +8610,20 @@ dependencies = [ [[package]] name = "wgpu" -version = "24.0.5" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" +checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", - "bitflags 2.9.1", + "bitflags 2.10.0", "cfg_aliases", "document-features", + "hashbrown 0.15.5", "js-sys", "log", + "naga", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", "smallvec", @@ -7622,46 +8638,84 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "24.0.5" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" +checksum = "f7b882196f8368511d613c6aeec80655160db6646aebddf8328879a88d54e500" dependencies = [ "arrayvec", + "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.9.1", + "bitflags 2.10.0", "cfg_aliases", "document-features", - "indexmap 2.9.0", + "hashbrown 0.15.5", + "indexmap 2.12.0", "log", "naga", "once_cell", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-types", ] +[[package]] +name = "wgpu-core-deps-apple" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd488b3239b6b7b185c3b045c39ca6bf8af34467a4c5de4e0b1a564135d093d" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09ad7aceb3818e52539acc679f049d3475775586f3f4e311c30165cf2c00445" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba5fb5f7f9c98baa7c889d444f63ace25574833df56f5b817985f641af58e46" +dependencies = [ + "wgpu-hal", +] + [[package]] name = "wgpu-hal" -version = "24.0.4" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +checksum = "f968767fe4d3d33747bbd1473ccd55bf0f6451f55d733b5597e67b5deab4ad17" dependencies = [ "android_system_properties", "arrayvec", "ash", - "bitflags 2.9.1", + "bit-set 0.8.0", + "bitflags 2.10.0", + "block", "bytemuck", + "cfg-if", "cfg_aliases", "core-graphics-types", "glow", "glutin_wgl_sys", "gpu-alloc", + "gpu-allocator", "gpu-descriptor", + "hashbrown 0.15.5", "js-sys", "khronos-egl", "libc", @@ -7671,30 +8725,33 @@ dependencies = [ "naga", "ndk-sys 0.5.0+25.2.9519653", "objc", - "once_cell", "ordered-float", "parking_lot", + "portable-atomic", "profiling", + "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "wasm-bindgen", "web-sys", "wgpu-types", - "windows", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] name = "wgpu-types" -version = "24.0.0" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", + "bytemuck", "js-sys", "log", + "thiserror 2.0.17", "web-sys", ] @@ -7706,7 +8763,18 @@ checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" dependencies = [ "either", "env_home", - "rustix 1.0.7", + "rustix 1.1.2", + "winsafe", +] + +[[package]] +name = "which" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +dependencies = [ + "env_home", + "rustix 1.1.2", "winsafe", ] @@ -7728,11 +8796,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7751,6 +8819,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.58.0" @@ -7770,13 +8860,37 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", - "windows-link", + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -7785,18 +8899,18 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -7807,18 +8921,18 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -7827,13 +8941,29 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] @@ -7853,7 +8983,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -7872,7 +9011,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -7884,6 +9032,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -7908,7 +9065,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -7959,18 +9125,28 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.1.3", ] [[package]] @@ -7993,9 +9169,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -8017,9 +9193,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -8041,9 +9217,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -8053,9 +9229,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -8077,9 +9253,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -8101,9 +9277,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -8125,9 +9301,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -8149,20 +9325,20 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" -version = "0.30.11" +version = "0.30.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" dependencies = [ - "ahash 0.8.12", + "ahash", "android-activity", "atomic-waker", - "bitflags 2.9.1", + "bitflags 2.10.0", "block2 0.5.1", "bytemuck", "calloop", @@ -8216,20 +9392,21 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] @@ -8239,26 +9416,140 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "winter-air" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef01227f23c7c331710f43b877a8333f5f8d539631eea763600f1a74bf018c7c" +dependencies = [ + "libm", + "winter-crypto", + "winter-fri", + "winter-math", + "winter-utils", +] + +[[package]] +name = "winter-crypto" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb247bc142438798edb04067ab72a22cf815f57abbd7b78a6fa986fc101db8" +dependencies = [ + "blake3", + "sha3", + "winter-math", + "winter-utils", +] + +[[package]] +name = "winter-fri" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "fd592b943f9d65545683868aaf1b601eb66e52bfd67175347362efff09101d3a" dependencies = [ - "bitflags 2.9.1", + "winter-crypto", + "winter-math", + "winter-utils", ] +[[package]] +name = "winter-math" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aecfb48ee6a8b4746392c8ff31e33e62df8528a3b5628c5af27b92b14aef1ea" +dependencies = [ + "winter-utils", +] + +[[package]] +name = "winter-maybe-async" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d31a19dae58475d019850e25b0170e94b16d382fbf6afee9c0e80fdc935e73e" +dependencies = [ + "quote", + "syn 2.0.108", +] + +[[package]] +name = "winter-prover" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84cc631ed56cd39b78ef932c1ec4060cc6a44d114474291216c32f56655b3048" +dependencies = [ + "tracing", + "winter-air", + "winter-crypto", + "winter-fri", + "winter-math", + "winter-maybe-async", + "winter-utils", +] + +[[package]] +name = "winter-utils" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9951263ef5317740cd0f49e618db00c72fabb70b75756ea26c4d5efe462c04dd" +dependencies = [ + "rayon", +] + +[[package]] +name = "winter-verifier" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0425ea81f8f703a1021810216da12003175c7974a584660856224df04b2e2fdb" +dependencies = [ + "winter-air", + "winter-crypto", + "winter-fri", + "winter-math", + "winter-utils", +] + +[[package]] +name = "winterfell" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f824ddd5aec8ca6a54307f20c115485a8a919ea94dd26d496d856ca6185f4f" +dependencies = [ + "winter-air", + "winter-prover", + "winter-verifier", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "withdrawals-contract" version = "2.0.1" -source = "git+https://github.com/dashpay/platform?rev=5f93c70720a1ea09f91c9142668e17f314986f03#5f93c70720a1ea09f91c9142668e17f314986f03" +source = "git+https://github.com/dashpay/platform?tag=v2.0.1#5f93c70720a1ea09f91c9142668e17f314986f03" dependencies = [ "num_enum 0.5.11", - "platform-value", - "platform-version", + "platform-value 2.0.1", + "platform-version 2.0.1", "serde", "serde_json", "serde_repr", - "thiserror 2.0.12", + "thiserror 2.0.17", +] + +[[package]] +name = "withdrawals-contract" +version = "2.1.0" +source = "git+https://www.github.com/dashpay/platform?rev=29f7492ed353d9cf2e09d0d4e6741d772e210d6b#29f7492ed353d9cf2e09d0d4e6741d772e210d6b" +dependencies = [ + "num_enum 0.5.11", + "platform-value 2.1.0", + "platform-version 2.1.0", + "serde", + "serde_json", + "serde_repr", + "thiserror 2.0.17", ] [[package]] @@ -8289,40 +9580,30 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", "libloading", "once_cell", - "rustix 0.38.44", + "rustix 1.1.2", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" [[package]] name = "xcursor" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" [[package]] name = "xkbcommon-dl" @@ -8330,7 +9611,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "dlib", "log", "once_cell", @@ -8345,9 +9626,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "yoke" @@ -8369,19 +9650,18 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", "synstructure", ] [[package]] name = "zbus" -version = "4.4.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" dependencies = [ "async-broadcast", "async-executor", - "async-fs", "async-io", "async-lock", "async-process", @@ -8390,121 +9670,61 @@ dependencies = [ "async-trait", "blocking", "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix 0.29.0", - "ordered-stream", - "rand 0.8.5", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "windows-sys 0.52.0", - "xdg-home", - "zbus_macros 4.4.0", - "zbus_names 3.0.0", - "zvariant 4.2.0", -] - -[[package]] -name = "zbus" -version = "5.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", + "event-listener 5.4.1", "futures-core", "futures-lite", "hex", - "nix 0.30.1", + "nix", "ordered-stream", "serde", "serde_repr", "tracing", "uds_windows", - "windows-sys 0.59.0", - "winnow 0.7.11", - "zbus_macros 5.7.1", - "zbus_names 4.2.0", - "zvariant 5.5.3", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", ] [[package]] name = "zbus-lockstep" -version = "0.4.4" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" +checksum = "29e96e38ded30eeab90b6ba88cb888d70aef4e7489b6cd212c5e5b5ec38045b6" dependencies = [ "zbus_xml", - "zvariant 4.2.0", + "zvariant", ] [[package]] name = "zbus-lockstep-macros" -version = "0.4.4" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" +checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", "zbus-lockstep", "zbus_xml", - "zvariant 4.2.0", -] - -[[package]] -name = "zbus_macros" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" -dependencies = [ - "proc-macro-crate 3.3.0", - "proc-macro2", - "quote", - "syn 2.0.103", - "zvariant_utils 2.1.0", + "zvariant", ] [[package]] name = "zbus_macros" -version = "5.7.1" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.103", - "zbus_names 4.2.0", - "zvariant 5.5.3", - "zvariant_utils 3.2.0", -] - -[[package]] -name = "zbus_names" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" -dependencies = [ - "serde", - "static_assertions", - "zvariant 4.2.0", + "syn 2.0.108", + "zbus_names", + "zvariant", + "zvariant_utils", ] [[package]] @@ -8515,41 +9735,41 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.11", - "zvariant 5.5.3", + "winnow 0.7.13", + "zvariant", ] [[package]] name = "zbus_xml" -version = "4.0.0" +version = "5.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" +checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" dependencies = [ - "quick-xml 0.30.0", + "quick-xml 0.36.2", "serde", "static_assertions", - "zbus_names 3.0.0", - "zvariant 4.2.0", + "zbus_names", + "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -8569,15 +9789,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "serde", "zeroize_derive", @@ -8591,7 +9811,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -8644,9 +9864,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -8661,7 +9881,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", ] [[package]] @@ -8675,12 +9895,32 @@ dependencies = [ "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.9.0", + "indexmap 2.12.0", + "memchr", + "thiserror 2.0.17", + "zopfli", +] + +[[package]] +name = "zip" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.12.0", "memchr", - "thiserror 2.0.12", "zopfli", ] +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + [[package]] name = "zmq" version = "0.10.0" @@ -8716,82 +9956,69 @@ dependencies = [ ] [[package]] -name = "zvariant" -version = "4.2.0" +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "zvariant_derive 4.2.0", + "cc", + "pkg-config", ] [[package]] -name = "zvariant" -version = "5.5.3" +name = "zune-core" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1" -dependencies = [ - "endi", - "enumflags2", - "serde", - "url", - "winnow 0.7.11", - "zvariant_derive 5.5.3", - "zvariant_utils 3.2.0", -] +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] -name = "zvariant_derive" -version = "4.2.0" +name = "zune-jpeg" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ - "proc-macro-crate 3.3.0", - "proc-macro2", - "quote", - "syn 2.0.103", - "zvariant_utils 2.1.0", + "zune-core", ] [[package]] -name = "zvariant_derive" -version = "5.5.3" +name = "zvariant" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" dependencies = [ - "proc-macro-crate 3.3.0", - "proc-macro2", - "quote", - "syn 2.0.103", - "zvariant_utils 3.2.0", + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", ] [[package]] -name = "zvariant_utils" -version = "2.1.0" +name = "zvariant_derive" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" dependencies = [ + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.108", + "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" dependencies = [ "proc-macro2", "quote", "serde", - "static_assertions", - "syn 2.0.103", - "winnow 0.7.11", + "syn 2.0.108", + "winnow 0.7.13", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d6d54aeae..4c32fb95e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,50 +1,54 @@ [package] name = "dash-evo-tool" -version = "0.9.2" +version = "0.9.3" license = "MIT" edition = "2024" default-run = "dash-evo-tool" -rust-version = "1.88" +rust-version = "1.89" [dependencies] tokio-util = { version = "0.7.15" } -bip39 = { version = "2.1.0", features = ["all-languages", "rand"] } +bip39 = { version = "2.2.0", features = ["all-languages", "rand"] } derive_more = "2.0.1" -egui = "0.31.1" -egui_extras = "0.31.1" -egui_commonmark = "0.20.0" -rfd = "0.15.3" +egui = "0.32.0" +egui_extras = "0.32.0" +egui_commonmark = "0.21.1" +rfd = "0.15.4" qrcode = "0.14.1" nix = { version = "0.30.1", features = ["signal"] } -eframe = { version = "0.31.1", features = ["persistence"] } +eframe = { version = "0.32.0", features = ["persistence"] } base64 = "0.22.1" -dash-sdk = { git = "https://github.com/dashpay/platform", rev = "5f93c70720a1ea09f91c9142668e17f314986f03" } +dash-sdk = { git = "https://www.github.com/dashpay/platform", rev = "29f7492ed353d9cf2e09d0d4e6741d772e210d6b", features = ["core_key_wallet", "core_bincode", "core_quorum-validation", "core_verification", "core_rpc_client"] } +grovestark = { git = "https://www.github.com/pauldelucia/grovestark", rev = "5313ba9df590f114e11934e281f1e8c8bc462794" } +rayon = "1.8" thiserror = "2.0.12" serde = "1.0.219" serde_json = "1.0.140" serde_yaml = { version = "0.9.34-deprecated" } -tokio = { version = "1.45.1", features = ["full"] } +tokio = { version = "1.46.1", features = ["full"] } bincode = { version = "=2.0.0-rc.3", features = ["serde"] } hex = { version = "0.4.3" } itertools = "0.14.0" enum-iterator = "2.1.0" futures = "0.3.31" tracing = "0.1.41" +rand = "0.8" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } dotenvy = "0.15.7" envy = "0.4.2" chrono = "0.4.41" chrono-humanize = "0.2.3" sha2 = "0.10.9" -arboard = { version = "3.5.0", default-features = false, features = [ +ed25519-dalek = "2.1" +arboard = { version = "3.6.0", default-features = false, features = [ "windows-sys", ] } directories = "6.0.0" -rusqlite = { version = "0.36.0", features = ["functions"] } -dark-light = "1.1.0" +rusqlite = { version = "0.37.0", features = ["functions"] } +dark-light = "2.0.0" image = { version = "0.25.6", default-features = false, features = ["png"] } bitflags = "2.9.1" -libsqlite3-sys = { version = "0.34.0", features = ["bundled"] } +libsqlite3-sys = { version = "0.35.0", features = ["bundled"] } rust-embed = "8.7.2" zeroize = "1.8.1" zxcvbn = "3.1.0" @@ -53,7 +57,7 @@ aes-gcm = "0.10.3" # For AES-256-GCM encryption crossbeam-channel = "0.5.15" regex = "1.11.1" humantime = "2.2.0" -which = { version = "7.0.3" } +which = { version = "8.0.0" } tz-rs = { version = "0.7.0" } [target.'cfg(not(target_os = "windows"))'.dependencies] @@ -67,9 +71,11 @@ native-dialog = "0.9.0" raw-cpuid = "11.5.0" [dev-dependencies] - tempfile = { version = "3.20.0" } -egui_kittest = { version = "0.31.1", features = ["eframe"] } +egui_kittest = { version = "0.32.0", features = ["eframe"] } [lints.clippy] uninlined_format_args = "allow" + +[patch.crates-io] +elliptic-curve-tools = { git = "https://github.com/mikelodder7/elliptic-curve-tools", rev = "c989865fa71503d2cbf5c5795c4ebcf4a2f3221c" } diff --git a/artifacts/mn_list_diff_0_2227096.bin b/artifacts/mn_list_diff_0_2227096.bin new file mode 100644 index 000000000..a75870c79 Binary files /dev/null and b/artifacts/mn_list_diff_0_2227096.bin differ diff --git a/artifacts/mn_list_diff_testnet_0_1296600.bin b/artifacts/mn_list_diff_testnet_0_1296600.bin new file mode 100644 index 000000000..dffaed42d Binary files /dev/null and b/artifacts/mn_list_diff_testnet_0_1296600.bin differ diff --git a/dash_core_configs/mainnet.conf b/dash_core_configs/mainnet.conf index e0f304586..882e2f51c 100644 --- a/dash_core_configs/mainnet.conf +++ b/dash_core_configs/mainnet.conf @@ -4,4 +4,4 @@ rpcuser=dashrpc rpcpassword=password server=1 zmqpubrawtxlocksig=tcp://0.0.0.0:23708 -zmqpubrawchainlock=tcp://0.0.0.0:23708 \ No newline at end of file +zmqpubrawchainlocksig=tcp://0.0.0.0:23708 \ No newline at end of file diff --git a/dash_core_configs/testnet.conf b/dash_core_configs/testnet.conf index c0c8019f6..a86132281 100644 --- a/dash_core_configs/testnet.conf +++ b/dash_core_configs/testnet.conf @@ -7,4 +7,4 @@ rpcuser=dashrpc rpcpassword=password server=1 zmqpubrawtxlocksig=tcp://0.0.0.0:23709 -zmqpubrawchainlock=tcp://0.0.0.0:23709 \ No newline at end of file +zmqpubrawchainlocksig=tcp://0.0.0.0:23709 \ No newline at end of file diff --git a/doc/COMPONENT_DESIGN_PATTERN.md b/doc/COMPONENT_DESIGN_PATTERN.md new file mode 100644 index 000000000..d1a6d73df --- /dev/null +++ b/doc/COMPONENT_DESIGN_PATTERN.md @@ -0,0 +1,100 @@ +# UI Component Design Pattern + +## Vision + +Imagine a library of ready-to-use widgets where you, as a developer, simply pick what you need. + +Need wallet selection? Grab `WalletChooserWidget`. It handles wallet selection, prompts for passwords when needed, validates user choices, and more. + +Need password entry? Use `PasswordWidget`. It manages passwords securely, masks input, validates complexity rules, and zeros memory after use. + +All widgets follow the same simple pattern: add 2 fields to your screen struct, lazy-load the widget, then bind it to your data with the `update()` method. + +## Quick Start: Using Components + +In this section, you will see how to use an existing component. + +### 1. Add fields to your screen struct +```rust +struct MyScreen { + amount: Option, // Domain data + amount_widget: Option, // UI component +} +``` + +### 2. Lazily initialize the component + +Inside your screen's `show()` method or simiar: + +```rust +let amount_widget = self.amount_widget.get_or_insert_with(|| { + AmountInput::new(amount_type) + .with_label("Amount:") +}); +``` + +### 3. Show component and handle updates + +After initialization above, use `update()` to bind your screen's field with the component: + +```rust +let response = amount_widget.show(ui); +response.inner.update(&mut self.amount); +``` + +### 4. Use the domain data +When `self.amount.is_some()`, the user has entered a valid amount. Use it for whatever you need. + +--- + +## Implementation Guidelines: Creating New Components + +In this screen, you will see generalized guidelines for creating a new component. + +### ✅ Component Structure Checklist +- [ ] Struct with private fields only +- [ ] `new()` constructor taking domain configuration +- [ ] Builder methods (`with_label()`, `with_max_amount()`, `with_hint_text()`, etc.) +- [ ] Response struct with `response`, `changed`, `error_message`, and domain-specific data fields + +### ✅ Trait Implementation Checklist +- [ ] Implement `Component` trait with `show()` method +- [ ] Implement `ComponentResponse` for response struct + +### ✅ Response Pattern +```rust +pub struct MyComponentResponse { + pub response: Response, + pub changed: bool, + pub error_message: Option, + // Add any component-specific fields as needed + pub parsed_data: Option, +} + +impl ComponentResponse for MyComponentResponse { + type DomainType = YourType; + + fn has_changed(&self) -> bool { self.changed } + fn is_valid(&self) -> bool { self.error_message.is_none() } + fn changed_value(&self) -> &Option { &self.parsed_data } + fn error_message(&self) -> Option<&str> { self.error_message.as_deref() } +} +``` + +### ✅ Best Practices +- [ ] Use lazy initialization (`Option`) +- [ ] Use egui's `add_enabled_ui()` for enabled/disabled state +- [ ] Set data to `None` when input changes but is invalid +- [ ] Provide fluent builder API for configuration +- [ ] Keep internal state private +- [ ] **Be self-contained**: Handle validation, error display, hints, and formatting internally (preferably with configurable error display) +- [ ] **Own your UX**: Component should manage its complete user experience +- [ ] Colors should be defined in `ComponentStyles` and optimized for light and dark mode + +### ❌ Anti-Patterns to Avoid +- Public mutable fields +- Managing enabled state in component +- Eager initialization +- Not clearing invalid data + +See `AmountInput` in `src/ui/components/amount_input.rs` for a complete example. diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c95c90571..4f3e8c521 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.88" +channel = "1.89" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 66e085a51..161c0ed58 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -2,7 +2,7 @@ name: dash-evo-tool title: Dash Evo Tool icon: mac_os/AppIcons/Assets.xcassets/AppIcon.appiconset/512.png type: app -version: "0.9.0-preview.4" +version: "0.9.3" summary: Graphical user interface for interacting with Dash Evolution description: | Dash Evo Tool is a graphical user interface for easily interacting with @@ -50,7 +50,7 @@ parts: - libssl-dev override-build: | # Install Rust - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.88 + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.89 export PATH="$HOME/.cargo/bin:$PATH" rustc --version cargo --version diff --git a/src/app.rs b/src/app.rs index 4c0f801f5..40e42d086 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,7 +9,9 @@ use crate::components::core_zmq_listener::{CoreZMQListener, ZMQMessage}; use crate::context::AppContext; use crate::database::Database; use crate::logging::initialize_logger; +use crate::model::settings::Settings; use crate::ui::contracts_documents::contracts_documents_screen::DocumentQueryScreen; +use crate::ui::contracts_documents::dashpay_coming_soon_screen::DashpayScreen; use crate::ui::dpns::dpns_contested_names_screen::{ DPNSScreen, DPNSSubscreen, ScheduledVoteCastingStatus, }; @@ -19,6 +21,8 @@ use crate::ui::theme::ThemeMode; use crate::ui::tokens::tokens_screen::{TokensScreen, TokensSubscreen}; use crate::ui::tools::contract_visualizer_screen::ContractVisualizerScreen; use crate::ui::tools::document_visualizer_screen::DocumentVisualizerScreen; +use crate::ui::tools::grovestark_screen::GroveSTARKScreen; +use crate::ui::tools::masternode_list_diff_screen::MasternodeListDiffScreen; use crate::ui::tools::platform_info_screen::PlatformInfoScreen; use crate::ui::tools::proof_log_screen::ProofLogScreen; use crate::ui::tools::proof_visualizer_screen::ProofVisualizerScreen; @@ -33,7 +37,6 @@ use derive_more::From; use eframe::{App, egui}; use std::collections::BTreeMap; use std::ops::BitOrAssign; -use std::path::PathBuf; use std::sync::{Arc, mpsc}; use std::time::{Duration, Instant, SystemTime}; use std::vec; @@ -155,14 +158,14 @@ impl AppState { let db = Arc::new(Database::new(&db_file_path).unwrap()); db.initialize(&db_file_path).unwrap(); - let settings = db.get_settings().expect("expected to get settings"); - - let (password_info, theme_preference) = - if let Some((_, _, password_info, _, _, theme_pref)) = settings.clone() { - (password_info, theme_pref) - } else { - (None, ThemeMode::System) // Default values if no settings found - }; + let settings = db + .get_settings() + .expect("expected to get settings") + .map(Settings::from) + .unwrap_or_default(); + let password_info = settings.password_info; + let theme_preference = settings.theme_mode; + let overwrite_dash_conf = settings.overwrite_dash_conf; let subtasks = Arc::new(TaskManager::new()); let mainnet_app_context = match AppContext::new( @@ -218,6 +221,7 @@ impl AppState { let mut contract_visualizer_screen = ContractVisualizerScreen::new(&mainnet_app_context); let mut proof_log_screen = ProofLogScreen::new(&mainnet_app_context); let mut platform_info_screen = PlatformInfoScreen::new(&mainnet_app_context); + let mut grovestark_screen = GroveSTARKScreen::new(&mainnet_app_context); let mut document_query_screen = DocumentQueryScreen::new(&mainnet_app_context); let mut tokens_balances_screen = TokensScreen::new(&mainnet_app_context, TokensSubscreen::MyTokens); @@ -225,18 +229,7 @@ impl AppState { TokensScreen::new(&mainnet_app_context, TokensSubscreen::SearchTokens); let mut token_creator_screen = TokensScreen::new(&mainnet_app_context, TokensSubscreen::TokenCreator); - - let (custom_dash_qt_path, overwrite_dash_conf) = match settings.clone() { - Some((.., custom_dash_qt_path, db_overwrite_dash_conf, _theme_pref)) => { - // Use the stored settings - let custom_dash_qt_path = custom_dash_qt_path.or_else(detect_dash_qt_path); - (custom_dash_qt_path, db_overwrite_dash_conf) - } - None => { - // Only use defaults if there are no settings at all - (None, true) - } - }; + let mut contracts_dashpay_screen = DashpayScreen::new(&mainnet_app_context); let mut network_chooser_screen = NetworkChooserScreen::new( &mainnet_app_context, @@ -244,95 +237,96 @@ impl AppState { devnet_app_context.as_ref(), local_app_context.as_ref(), Network::Dash, - custom_dash_qt_path, overwrite_dash_conf, ); + let mut masternode_list_diff_screen = MasternodeListDiffScreen::new(&mainnet_app_context); + let mut wallets_balances_screen = WalletsBalancesScreen::new(&mainnet_app_context); - let mut selected_main_screen = RootScreenType::RootScreenIdentities; - - let mut chosen_network = Network::Dash; - - if let Some((network, screen_type, _password_info, _, _, _)) = settings { - selected_main_screen = screen_type; - chosen_network = network; - network_chooser_screen.current_network = chosen_network; - - if chosen_network == Network::Testnet && testnet_app_context.is_some() { - let testnet_app_context = testnet_app_context.as_ref().unwrap(); - identities_screen = IdentitiesScreen::new(testnet_app_context); - dpns_active_contests_screen = - DPNSScreen::new(testnet_app_context, DPNSSubscreen::Active); - dpns_past_contests_screen = - DPNSScreen::new(testnet_app_context, DPNSSubscreen::Past); - dpns_my_usernames_screen = - DPNSScreen::new(testnet_app_context, DPNSSubscreen::Owned); - dpns_scheduled_votes_screen = - DPNSScreen::new(testnet_app_context, DPNSSubscreen::ScheduledVotes); - transition_visualizer_screen = TransitionVisualizerScreen::new(testnet_app_context); - proof_visualizer_screen = ProofVisualizerScreen::new(testnet_app_context); - document_visualizer_screen = DocumentVisualizerScreen::new(testnet_app_context); - contract_visualizer_screen = ContractVisualizerScreen::new(testnet_app_context); - document_query_screen = DocumentQueryScreen::new(testnet_app_context); - wallets_balances_screen = WalletsBalancesScreen::new(testnet_app_context); - proof_log_screen = ProofLogScreen::new(testnet_app_context); - platform_info_screen = PlatformInfoScreen::new(testnet_app_context); - tokens_balances_screen = - TokensScreen::new(testnet_app_context, TokensSubscreen::MyTokens); - token_search_screen = - TokensScreen::new(testnet_app_context, TokensSubscreen::SearchTokens); - token_creator_screen = - TokensScreen::new(testnet_app_context, TokensSubscreen::TokenCreator); - } else if chosen_network == Network::Devnet && devnet_app_context.is_some() { - let devnet_app_context = devnet_app_context.as_ref().unwrap(); - identities_screen = IdentitiesScreen::new(devnet_app_context); - dpns_active_contests_screen = - DPNSScreen::new(devnet_app_context, DPNSSubscreen::Active); - dpns_past_contests_screen = - DPNSScreen::new(devnet_app_context, DPNSSubscreen::Past); - dpns_my_usernames_screen = - DPNSScreen::new(devnet_app_context, DPNSSubscreen::Owned); - dpns_scheduled_votes_screen = - DPNSScreen::new(devnet_app_context, DPNSSubscreen::ScheduledVotes); - transition_visualizer_screen = TransitionVisualizerScreen::new(devnet_app_context); - proof_visualizer_screen = ProofVisualizerScreen::new(devnet_app_context); - document_visualizer_screen = DocumentVisualizerScreen::new(devnet_app_context); - document_query_screen = DocumentQueryScreen::new(devnet_app_context); - contract_visualizer_screen = ContractVisualizerScreen::new(devnet_app_context); - wallets_balances_screen = WalletsBalancesScreen::new(devnet_app_context); - proof_log_screen = ProofLogScreen::new(devnet_app_context); - platform_info_screen = PlatformInfoScreen::new(devnet_app_context); - tokens_balances_screen = - TokensScreen::new(devnet_app_context, TokensSubscreen::MyTokens); - token_search_screen = - TokensScreen::new(devnet_app_context, TokensSubscreen::SearchTokens); - token_creator_screen = - TokensScreen::new(devnet_app_context, TokensSubscreen::TokenCreator); - } else if chosen_network == Network::Regtest && local_app_context.is_some() { - let local_app_context = local_app_context.as_ref().unwrap(); - identities_screen = IdentitiesScreen::new(local_app_context); - dpns_active_contests_screen = - DPNSScreen::new(local_app_context, DPNSSubscreen::Active); - dpns_past_contests_screen = DPNSScreen::new(local_app_context, DPNSSubscreen::Past); - dpns_my_usernames_screen = DPNSScreen::new(local_app_context, DPNSSubscreen::Owned); - dpns_scheduled_votes_screen = - DPNSScreen::new(local_app_context, DPNSSubscreen::ScheduledVotes); - transition_visualizer_screen = TransitionVisualizerScreen::new(local_app_context); - proof_visualizer_screen = ProofVisualizerScreen::new(local_app_context); - document_visualizer_screen = DocumentVisualizerScreen::new(local_app_context); - contract_visualizer_screen = ContractVisualizerScreen::new(local_app_context); - document_query_screen = DocumentQueryScreen::new(local_app_context); - wallets_balances_screen = WalletsBalancesScreen::new(local_app_context); - proof_log_screen = ProofLogScreen::new(local_app_context); - platform_info_screen = PlatformInfoScreen::new(local_app_context); - tokens_balances_screen = - TokensScreen::new(local_app_context, TokensSubscreen::MyTokens); - token_search_screen = - TokensScreen::new(local_app_context, TokensSubscreen::SearchTokens); - token_creator_screen = - TokensScreen::new(local_app_context, TokensSubscreen::TokenCreator); - } + let selected_main_screen = settings.root_screen_type; + let chosen_network = settings.network; + network_chooser_screen.current_network = chosen_network; + + if let (Network::Testnet, Some(testnet_app_context)) = + (chosen_network, testnet_app_context.as_ref()) + { + identities_screen = IdentitiesScreen::new(testnet_app_context); + dpns_active_contests_screen = + DPNSScreen::new(testnet_app_context, DPNSSubscreen::Active); + dpns_past_contests_screen = DPNSScreen::new(testnet_app_context, DPNSSubscreen::Past); + dpns_my_usernames_screen = DPNSScreen::new(testnet_app_context, DPNSSubscreen::Owned); + dpns_scheduled_votes_screen = + DPNSScreen::new(testnet_app_context, DPNSSubscreen::ScheduledVotes); + transition_visualizer_screen = TransitionVisualizerScreen::new(testnet_app_context); + proof_visualizer_screen = ProofVisualizerScreen::new(testnet_app_context); + document_visualizer_screen = DocumentVisualizerScreen::new(testnet_app_context); + contract_visualizer_screen = ContractVisualizerScreen::new(testnet_app_context); + document_query_screen = DocumentQueryScreen::new(testnet_app_context); + grovestark_screen = GroveSTARKScreen::new(testnet_app_context); + wallets_balances_screen = WalletsBalancesScreen::new(testnet_app_context); + proof_log_screen = ProofLogScreen::new(testnet_app_context); + platform_info_screen = PlatformInfoScreen::new(testnet_app_context); + masternode_list_diff_screen = MasternodeListDiffScreen::new(testnet_app_context); + contracts_dashpay_screen = DashpayScreen::new(testnet_app_context); + tokens_balances_screen = + TokensScreen::new(testnet_app_context, TokensSubscreen::MyTokens); + token_search_screen = + TokensScreen::new(testnet_app_context, TokensSubscreen::SearchTokens); + token_creator_screen = + TokensScreen::new(testnet_app_context, TokensSubscreen::TokenCreator); + } else if let (Network::Devnet, Some(devnet_app_context)) = + (chosen_network, devnet_app_context.as_ref()) + { + identities_screen = IdentitiesScreen::new(devnet_app_context); + dpns_active_contests_screen = + DPNSScreen::new(devnet_app_context, DPNSSubscreen::Active); + dpns_past_contests_screen = DPNSScreen::new(devnet_app_context, DPNSSubscreen::Past); + dpns_my_usernames_screen = DPNSScreen::new(devnet_app_context, DPNSSubscreen::Owned); + dpns_scheduled_votes_screen = + DPNSScreen::new(devnet_app_context, DPNSSubscreen::ScheduledVotes); + transition_visualizer_screen = TransitionVisualizerScreen::new(devnet_app_context); + proof_visualizer_screen = ProofVisualizerScreen::new(devnet_app_context); + document_visualizer_screen = DocumentVisualizerScreen::new(devnet_app_context); + document_query_screen = DocumentQueryScreen::new(devnet_app_context); + masternode_list_diff_screen = MasternodeListDiffScreen::new(devnet_app_context); + contract_visualizer_screen = ContractVisualizerScreen::new(devnet_app_context); + grovestark_screen = GroveSTARKScreen::new(devnet_app_context); + wallets_balances_screen = WalletsBalancesScreen::new(devnet_app_context); + proof_log_screen = ProofLogScreen::new(devnet_app_context); + platform_info_screen = PlatformInfoScreen::new(devnet_app_context); + tokens_balances_screen = + TokensScreen::new(devnet_app_context, TokensSubscreen::MyTokens); + token_search_screen = + TokensScreen::new(devnet_app_context, TokensSubscreen::SearchTokens); + token_creator_screen = + TokensScreen::new(devnet_app_context, TokensSubscreen::TokenCreator); + } else if let (Network::Regtest, Some(local_app_context)) = + (chosen_network, local_app_context.as_ref()) + { + identities_screen = IdentitiesScreen::new(local_app_context); + dpns_active_contests_screen = DPNSScreen::new(local_app_context, DPNSSubscreen::Active); + dpns_past_contests_screen = DPNSScreen::new(local_app_context, DPNSSubscreen::Past); + dpns_my_usernames_screen = DPNSScreen::new(local_app_context, DPNSSubscreen::Owned); + dpns_scheduled_votes_screen = + DPNSScreen::new(local_app_context, DPNSSubscreen::ScheduledVotes); + transition_visualizer_screen = TransitionVisualizerScreen::new(local_app_context); + proof_visualizer_screen = ProofVisualizerScreen::new(local_app_context); + document_visualizer_screen = DocumentVisualizerScreen::new(local_app_context); + contract_visualizer_screen = ContractVisualizerScreen::new(local_app_context); + document_query_screen = DocumentQueryScreen::new(local_app_context); + grovestark_screen = GroveSTARKScreen::new(local_app_context); + wallets_balances_screen = WalletsBalancesScreen::new(local_app_context); + masternode_list_diff_screen = MasternodeListDiffScreen::new(local_app_context); + proof_log_screen = ProofLogScreen::new(local_app_context); + platform_info_screen = PlatformInfoScreen::new(local_app_context); + contracts_dashpay_screen = DashpayScreen::new(local_app_context); + tokens_balances_screen = + TokensScreen::new(local_app_context, TokensSubscreen::MyTokens); + token_search_screen = + TokensScreen::new(local_app_context, TokensSubscreen::SearchTokens); + token_creator_screen = + TokensScreen::new(local_app_context, TokensSubscreen::TokenCreator); } // // Create a channel with a buffer size of 32 (adjust as needed) @@ -343,9 +337,16 @@ impl AppState { let (core_message_sender, core_message_receiver) = mpsc::channel().with_egui_ctx(ctx.clone()); + let mainnet_core_zmq_endpoint = mainnet_app_context + .config + .read() + .unwrap() + .core_zmq_endpoint + .clone() + .unwrap_or_else(|| "tcp://127.0.0.1:23708".to_string()); let mainnet_core_zmq_listener = CoreZMQListener::spawn_listener( Network::Dash, - "tcp://127.0.0.1:23708", + &mainnet_core_zmq_endpoint, core_message_sender.clone(), // Clone the sender for each listener Some(mainnet_app_context.sx_zmq_status.clone()), ) @@ -355,9 +356,13 @@ impl AppState { .as_ref() .map(|context| context.sx_zmq_status.clone()); + let testnet_core_zmq_endpoint = testnet_app_context + .as_ref() + .and_then(|ctx| ctx.config.read().unwrap().core_zmq_endpoint.clone()) + .unwrap_or_else(|| "tcp://127.0.0.1:23709".to_string()); let testnet_core_zmq_listener = CoreZMQListener::spawn_listener( Network::Testnet, - "tcp://127.0.0.1:23709", + &testnet_core_zmq_endpoint, core_message_sender.clone(), // Use the original sender or create a new one if needed testnet_tx_zmq_status_option, ) @@ -367,9 +372,13 @@ impl AppState { .as_ref() .map(|context| context.sx_zmq_status.clone()); + let devnet_core_zmq_endpoint = devnet_app_context + .as_ref() + .and_then(|ctx| ctx.config.read().unwrap().core_zmq_endpoint.clone()) + .unwrap_or_else(|| "tcp://127.0.0.1:23710".to_string()); let devnet_core_zmq_listener = CoreZMQListener::spawn_listener( Network::Devnet, - "tcp://127.0.0.1:23710", + &devnet_core_zmq_endpoint, core_message_sender.clone(), devnet_tx_zmq_status_option, ) @@ -379,9 +388,13 @@ impl AppState { .as_ref() .map(|context| context.sx_zmq_status.clone()); + let local_core_zmq_endpoint = local_app_context + .as_ref() + .and_then(|ctx| ctx.config.read().unwrap().core_zmq_endpoint.clone()) + .unwrap_or_else(|| "tcp://127.0.0.1:20302".to_string()); let local_core_zmq_listener = CoreZMQListener::spawn_listener( Network::Regtest, - "tcp://127.0.0.1:20302", + &local_core_zmq_endpoint, core_message_sender, local_tx_zmq_status_option, ) @@ -437,14 +450,26 @@ impl AppState { RootScreenType::RootScreenToolsPlatformInfoScreen, Screen::PlatformInfoScreen(platform_info_screen), ), + ( + RootScreenType::RootScreenToolsGroveSTARKScreen, + Screen::GroveSTARKScreen(grovestark_screen), + ), ( RootScreenType::RootScreenDocumentQuery, Screen::DocumentQueryScreen(document_query_screen), ), + ( + RootScreenType::RootScreenDashpay, + Screen::DashpayScreen(contracts_dashpay_screen), + ), ( RootScreenType::RootScreenNetworkChooser, Screen::NetworkChooserScreen(network_chooser_screen), ), + ( + RootScreenType::RootScreenToolsMasternodeListDiffScreen, + Screen::MasternodeListDiffScreen(masternode_list_diff_screen), + ), ( RootScreenType::RootScreenMyTokenBalances, Screen::TokensScreen(Box::new(tokens_balances_screen)), @@ -612,10 +637,10 @@ impl App for AppState { // Apply Dash theme with user preference crate::ui::theme::apply_theme(ctx, self.theme_preference); - if let Ok(event) = self.current_app_context().rx_zmq_status.try_recv() { - if let Ok(mut status) = self.current_app_context().zmq_connection_status.lock() { - *status = event; - } + if let Ok(event) = self.current_app_context().rx_zmq_status.try_recv() + && let Ok(mut status) = self.current_app_context().zmq_connection_status.lock() + { + *status = event; } // Poll the receiver for any new task results @@ -700,10 +725,14 @@ impl App for AppState { match message { ZMQMessage::ISLockedTransaction(tx, is_lock) => { // Store the asset lock transaction in the database - match app_context.received_transaction_finality(&tx, Some(is_lock), None) { + match app_context.received_transaction_finality( + &tx, + Some(is_lock.clone()), + None, + ) { Ok(utxos) => { let core_item = - CoreItem::ReceivedAvailableUTXOTransaction(tx.clone(), utxos); + CoreItem::InstantLockedTransaction(tx.clone(), utxos, is_lock); self.visible_screen_mut() .display_task_result(BackendTaskSuccessResult::CoreItem(core_item)); } @@ -719,7 +748,13 @@ impl App for AppState { eprintln!("Failed to store asset lock: {}", e); } } - ZMQMessage::ChainLockedBlock(_) => {} + ZMQMessage::ChainLockedBlock(block, chain_lock) => { + self.visible_screen_mut().display_task_result( + BackendTaskSuccessResult::CoreItem(CoreItem::ChainLockedBlock( + block, chain_lock, + )), + ); + } } } @@ -883,31 +918,3 @@ impl App for AppState { // } } } - -pub(crate) fn detect_dash_qt_path() -> Option { - let path = which::which("dash-qt") - .map(|path| path.to_string_lossy().to_string()) - .inspect_err(|e| tracing::warn!("failed to find dash-qt: {}", e)) - .ok() - .map(PathBuf::from) - .unwrap_or_else(|| { - // Fallback to default paths based on the operating system - if cfg!(target_os = "macos") { - PathBuf::from("/Applications/Dash-Qt.app/Contents/MacOS/Dash-Qt") - } else if cfg!(target_os = "windows") { - // Retrieve the PROGRAMFILES environment variable or default to "C:\\Program Files" - let program_files = std::env::var("PROGRAMFILES") - .unwrap_or_else(|_| "C:\\Program Files".to_string()); - PathBuf::from(program_files).join("DashCore\\dash-qt.exe") - } else { - PathBuf::from("/usr/local/bin/dash-qt") // Default Linux path - } - }); - - if path.is_file() { - Some(path) - } else { - tracing::warn!("Dash-Qt binary not found at: {:?}", path); - None - } -} diff --git a/src/backend_task/core/mod.rs b/src/backend_task/core/mod.rs index 72f168837..170c61d20 100644 --- a/src/backend_task/core/mod.rs +++ b/src/backend_task/core/mod.rs @@ -8,7 +8,9 @@ use crate::context::AppContext; use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::RpcApi; use dash_sdk::dashcore_rpc::{Auth, Client}; -use dash_sdk::dpp::dashcore::{Address, ChainLock, Network, OutPoint, Transaction, TxOut}; +use dash_sdk::dpp::dashcore::{ + Address, Block, ChainLock, InstantLock, Network, OutPoint, Transaction, TxOut, +}; use std::path::PathBuf; use std::sync::{Arc, RwLock}; @@ -40,6 +42,7 @@ impl PartialEq for CoreTask { #[derive(Debug, Clone, PartialEq)] pub enum CoreItem { + InstantLockedTransaction(Transaction, Vec<(OutPoint, TxOut, Address)>, InstantLock), ReceivedAvailableUTXOTransaction(Transaction, Vec<(OutPoint, TxOut, Address)>), ChainLock(ChainLock, Network), ChainLocks( @@ -48,6 +51,7 @@ pub enum CoreItem { Option, Option, ), // Mainnet, Testnet, Devnet, Local + ChainLockedBlock(Block, ChainLock), } impl AppContext { diff --git a/src/backend_task/grovestark.rs b/src/backend_task/grovestark.rs new file mode 100644 index 000000000..3a2487ee0 --- /dev/null +++ b/src/backend_task/grovestark.rs @@ -0,0 +1,65 @@ +use crate::backend_task::BackendTaskSuccessResult; +use crate::model::grovestark_prover::{GroveSTARKProver, ProofDataOutput}; +use dash_sdk::Sdk; + +pub async fn run_grovestark_task( + task: GroveSTARKTask, + sdk: &Sdk, +) -> Result { + match task { + GroveSTARKTask::GenerateProof { + identity_id, + contract_id, + document_type, + document_id, + key_id, + private_key, + public_key, + } => { + let prover = GroveSTARKProver::new(); + + match prover + .generate_proof( + sdk, + &identity_id, + &contract_id, + &document_type, + &document_id, + key_id, + &private_key, + &public_key, + ) + .await + { + Ok(proof_data) => Ok(BackendTaskSuccessResult::GeneratedZKProof(proof_data)), + Err(e) => Err(format!("Failed to generate proof: {}", e)), + } + } + GroveSTARKTask::VerifyProof { proof_data } => { + let prover = GroveSTARKProver::new(); + + match prover.verify_proof(&proof_data) { + Ok(is_valid) => Ok(BackendTaskSuccessResult::VerifiedZKProof( + is_valid, proof_data, + )), + Err(e) => Err(format!("Failed to verify proof: {}", e)), + } + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum GroveSTARKTask { + GenerateProof { + identity_id: String, + contract_id: String, + document_type: String, + document_id: String, + key_id: u32, + private_key: [u8; 32], + public_key: [u8; 32], + }, + VerifyProof { + proof_data: ProofDataOutput, + }, +} diff --git a/src/backend_task/identity/load_identity.rs b/src/backend_task/identity/load_identity.rs index 0947ca1e0..b82da7ad9 100644 --- a/src/backend_task/identity/load_identity.rs +++ b/src/backend_task/identity/load_identity.rs @@ -69,8 +69,9 @@ impl AppContext { let wallets = self.wallets.read().unwrap().clone(); - if identity_type != IdentityType::User && owner_private_key_bytes.is_some() { - let owner_private_key_bytes = owner_private_key_bytes.unwrap(); + if identity_type != IdentityType::User + && let Some(owner_private_key_bytes) = owner_private_key_bytes + { let key = self.verify_owner_key_exists_on_identity(&identity, &owner_private_key_bytes)?; let key_id = key.id(); @@ -89,8 +90,9 @@ impl AppContext { ); } - if identity_type != IdentityType::User && payout_address_private_key_bytes.is_some() { - let payout_address_private_key_bytes = payout_address_private_key_bytes.unwrap(); + if identity_type != IdentityType::User + && let Some(payout_address_private_key_bytes) = payout_address_private_key_bytes + { let key = self.verify_payout_address_key_exists_on_identity( &identity, &payout_address_private_key_bytes, @@ -112,46 +114,49 @@ impl AppContext { } // If the identity type is not a User, and we have a voting private key, verify it - let associated_voter_identity = if identity_type != IdentityType::User - && voting_private_key_bytes.is_some() - { - let voting_private_key_bytes = voting_private_key_bytes.unwrap(); - if let Ok(private_key) = - PrivateKey::from_slice(voting_private_key_bytes.as_slice(), self.network) - { - // Make the vote identifier - let address = private_key.public_key(&Secp256k1::new()).pubkey_hash(); - let voter_identifier = - Identifier::create_voter_identifier(identity_id.as_bytes(), address.as_ref()); + let associated_voter_identity = if identity_type != IdentityType::User { + if let Some(voting_private_key_bytes) = voting_private_key_bytes { + if let Ok(private_key) = + PrivateKey::from_byte_array(&voting_private_key_bytes, self.network) + { + // Make the vote identifier + let address = private_key.public_key(&Secp256k1::new()).pubkey_hash(); + let voter_identifier = Identifier::create_voter_identifier( + identity_id.as_bytes(), + address.as_ref(), + ); - // Fetch the voter identifier - let voter_identity = - match Identity::fetch_by_identifier(sdk, voter_identifier).await { - Ok(Some(identity)) => identity, - Ok(None) => return Err("Voter Identity not found".to_string()), - Err(e) => return Err(format!("Error fetching voter identity: {}", e)), - }; + // Fetch the voter identifier + let voter_identity = + match Identity::fetch_by_identifier(sdk, voter_identifier).await { + Ok(Some(identity)) => identity, + Ok(None) => return Err("Voter Identity not found".to_string()), + Err(e) => return Err(format!("Error fetching voter identity: {}", e)), + }; - let key = self.verify_voting_key_exists_on_identity( - &voter_identity, - &voting_private_key_bytes, - )?; - let qualified_key = - QualifiedIdentityPublicKey::from_identity_public_key_with_wallets_check( - key.clone(), - self.network, - &wallets.values().collect::>(), + let key = self.verify_voting_key_exists_on_identity( + &voter_identity, + &voting_private_key_bytes, + )?; + let qualified_key = + QualifiedIdentityPublicKey::from_identity_public_key_with_wallets_check( + key.clone(), + self.network, + &wallets.values().collect::>(), + ); + encrypted_private_keys.insert( + (PrivateKeyOnVoterIdentity, key.id()), + ( + qualified_key, + PrivateKeyData::Clear(voting_private_key_bytes), + ), ); - encrypted_private_keys.insert( - (PrivateKeyOnVoterIdentity, key.id()), - ( - qualified_key, - PrivateKeyData::Clear(voting_private_key_bytes), - ), - ); - Some((voter_identity, key)) + Some((voter_identity, key)) + } else { + return Err("Voting private key is not valid".to_string()); + } } else { - return Err("Voting private key is not valid".to_string()); + None } } else { None @@ -167,7 +172,7 @@ impl AppContext { verify_key_input(key_string, "User Key") .transpose()? .and_then(|sk| { - PrivateKey::from_slice(sk.as_slice(), self.network) + PrivateKey::from_byte_array(&sk, self.network) .map_err(|e| e.to_string()) }), ) @@ -304,6 +309,7 @@ impl AppContext { wallet_index: None, //todo top_ups: Default::default(), status: IdentityStatus::Active, + network: self.network, }; let wallet_info = qualified_identity.determine_wallet_info()?; diff --git a/src/backend_task/identity/load_identity_from_wallet.rs b/src/backend_task/identity/load_identity_from_wallet.rs index bbe630e21..4d1b589c4 100644 --- a/src/backend_task/identity/load_identity_from_wallet.rs +++ b/src/backend_task/identity/load_identity_from_wallet.rs @@ -1,4 +1,5 @@ use super::{BackendTaskSuccessResult, IdentityIndex}; +use crate::app::TaskResult; use crate::context::AppContext; use crate::model::qualified_identity::encrypted_key_storage::{ PrivateKeyData, WalletDerivationPath, @@ -9,15 +10,15 @@ use crate::model::qualified_identity::{ }; use crate::model::wallet::WalletArcRef; use dash_sdk::Sdk; -use dash_sdk::dpp::dashcore::bip32::{DerivationPath, KeyDerivationType}; -use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::document::DocumentV0Getters; +use dash_sdk::dpp::identity::KeyType; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::hash::IdentityPublicKeyHashMethodsV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; -use dash_sdk::dpp::identity::{KeyID, KeyType}; +use dash_sdk::dpp::key_wallet::bip32::{DerivationPath, KeyDerivationType}; use dash_sdk::dpp::platform_value::Value; use dash_sdk::drive::query::{WhereClause, WhereOperator}; -use dash_sdk::platform::types::identity::PublicKeyHash; +use dash_sdk::platform::types::identity::NonUniquePublicKeyHashQuery; use dash_sdk::platform::{Document, DocumentQuery, Fetch, FetchMany, Identity}; use std::collections::BTreeMap; @@ -27,19 +28,83 @@ impl AppContext { sdk: &Sdk, wallet_arc_ref: WalletArcRef, identity_index: IdentityIndex, + sender: crate::utils::egui_mpsc::SenderAsync, ) -> Result { - let public_key = { - let wallet = wallet_arc_ref.wallet.write().unwrap(); - wallet.identity_authentication_ecdsa_public_key(self.network, identity_index, 0)? - }; + const AUTH_KEY_LOOKUP_WINDOW: u32 = 12; + + let mut fetched_identity: Option = None; + let mut queried_public_key = None; + let mut queried_wallet_key_index = None; + + for key_index in 0..AUTH_KEY_LOOKUP_WINDOW { + let public_key = { + let wallet = wallet_arc_ref.wallet.write().unwrap(); + wallet.identity_authentication_ecdsa_public_key( + self.network, + identity_index, + key_index, + )? + }; - let Some(identity) = - Identity::fetch(sdk, PublicKeyHash(public_key.pubkey_hash().to_byte_array())) + let key_hash = public_key.pubkey_hash().into(); + let query = NonUniquePublicKeyHashQuery { + key_hash, + after: None, + }; + + sender + .send(TaskResult::Success(Box::new( + BackendTaskSuccessResult::Message(format!( + "Searching for identity using key at index {}...", + key_index + )), + ))) .await - .map_err(|e| e.to_string())? - else { - return Ok(BackendTaskSuccessResult::None); + .map_err(|e| e.to_string())?; + match Identity::fetch(sdk, query).await { + Ok(Some(identity)) => { + fetched_identity = Some(identity); + queried_public_key = Some(public_key); + queried_wallet_key_index = Some(key_index); + break; + } + Ok(None) => continue, + Err(e) => return Err(e.to_string()), + } + } + + let identity = match fetched_identity { + Some(identity) => identity, + None => { + return Err(format!( + "No identity found for wallet identity index {} within the first {} derived authentication keys", + identity_index, AUTH_KEY_LOOKUP_WINDOW + )); + } + }; + + let queried_public_key = + queried_public_key.expect("queried public key should exist when identity is fetched"); + let queried_wallet_key_index = queried_wallet_key_index + .expect("wallet key index should exist when identity is fetched"); + + let queried_key_hash: [u8; 20] = queried_public_key.pubkey_hash().into(); + let matching_identity_key = identity.public_keys().values().find(|key| { + key.public_key_hash() + .ok() + .map(|hash| hash == queried_key_hash) + .unwrap_or(false) + }); + + let matching_identity_key = match matching_identity_key { + Some(key) => key, + None => { + return Err( + "Fetched identity does not contain the queried authentication key".to_string(), + ); + } }; + let matching_identity_key_id = matching_identity_key.id(); let identity_id = identity.id(); @@ -91,7 +156,16 @@ impl AppContext { }) .map_err(|e| format!("Error fetching DPNS names: {}", e))?; - let top_bound = identity.public_keys().len() as u32 + 5; + let highest_identity_key_id = identity + .public_keys() + .keys() + .copied() + .max() + .unwrap_or(matching_identity_key_id); + + let mut top_bound = highest_identity_key_id.saturating_add(1); + top_bound = top_bound.max(queried_wallet_key_index.saturating_add(1)); + top_bound = top_bound.saturating_add(5); let wallet_seed_hash; let (public_key_result_map, public_key_hash_result_map) = { @@ -105,45 +179,85 @@ impl AppContext { )? }; - let private_keys = identity.public_keys().values().filter_map(|public_key| { - let index: u32 = match public_key.key_type() { - KeyType::ECDSA_SECP256K1 => { - public_key_result_map.get(public_key.data().as_slice()).cloned() - } - KeyType::ECDSA_HASH160 => { - let hash: [u8;20] = public_key.data().as_slice().try_into().ok()?; - public_key_hash_result_map.get(&hash).cloned() - } - _ => None, - }?; - let derivation_path = DerivationPath::identity_authentication_path( - self.network, - KeyDerivationType::ECDSA, - identity_index, - index, + let private_keys_map = identity + .public_keys() + .values() + .filter_map(|public_key| { + let index: u32 = match public_key.key_type() { + KeyType::ECDSA_SECP256K1 => public_key_result_map + .get(public_key.data().as_slice()) + .cloned(), + KeyType::ECDSA_HASH160 => { + let hash: [u8; 20] = public_key.data().as_slice().try_into().ok()?; + public_key_hash_result_map.get(&hash).cloned() + } + _ => None, + }?; + let derivation_path = DerivationPath::identity_authentication_path( + self.network, + KeyDerivationType::ECDSA, + identity_index, + index, + ); + let wallet_derivation_path = WalletDerivationPath { + wallet_seed_hash, + derivation_path, + }; + Some(( + (PrivateKeyTarget::PrivateKeyOnMainIdentity, public_key.id()), + ( + QualifiedIdentityPublicKey { + identity_public_key: public_key.clone(), + in_wallet_at_derivation_path: Some(wallet_derivation_path.clone()), + }, + PrivateKeyData::AtWalletDerivationPath(wallet_derivation_path), + ), + )) + }) + .collect::>(); + + if private_keys_map.is_empty() { + return Err("Could not match any identity keys to wallet derivation paths".to_string()); + } + + if !private_keys_map.contains_key(&( + PrivateKeyTarget::PrivateKeyOnMainIdentity, + matching_identity_key_id, + )) { + return Err( + "Unable to locate wallet derivation path for the queried identity key".to_string(), ); - let wallet_derivation_path = WalletDerivationPath { wallet_seed_hash, derivation_path}; - Some(((PrivateKeyTarget::PrivateKeyOnMainIdentity, public_key.id()), (QualifiedIdentityPublicKey { identity_public_key: public_key.clone(), in_wallet_at_derivation_path: Some(wallet_derivation_path.clone()) }, PrivateKeyData::AtWalletDerivationPath(wallet_derivation_path)))) - }).collect::>().into(); + } + + let private_keys = private_keys_map.into(); - let qualified_identity = QualifiedIdentity { - identity, + let wallet_seed_hash = wallet_arc_ref.wallet.read().unwrap().seed_hash(); + + let mut qualified_identity = QualifiedIdentity { + identity: identity.clone(), associated_voter_identity: None, associated_operator_identity: None, associated_owner_key_id: None, identity_type: IdentityType::User, alias: None, - private_keys, - dpns_names: maybe_owned_dpns_names, - associated_wallets: BTreeMap::from([( - wallet_arc_ref.wallet.read().unwrap().seed_hash(), - wallet_arc_ref.wallet.clone(), - )]), - wallet_index: Some(identity_index), + private_keys: Default::default(), + dpns_names: Vec::new(), + associated_wallets: BTreeMap::new(), + wallet_index: None, top_ups: Default::default(), status: IdentityStatus::Active, + network: self.network, }; + qualified_identity.identity = identity; + qualified_identity.private_keys = private_keys; + qualified_identity.dpns_names = maybe_owned_dpns_names; + qualified_identity.associated_wallets = + BTreeMap::from([(wallet_seed_hash, wallet_arc_ref.wallet.clone())]); + qualified_identity.wallet_index = Some(identity_index); + qualified_identity.status = IdentityStatus::Active; + qualified_identity.network = self.network; + // Insert qualified identity into the database self.insert_local_qualified_identity( &qualified_identity, @@ -151,8 +265,108 @@ impl AppContext { ) .map_err(|e| format!("Database error: {}", e))?; + { + let mut wallet = wallet_arc_ref.wallet.write().unwrap(); + wallet + .identities + .insert(identity_index, qualified_identity.identity.clone()); + } + Ok(BackendTaskSuccessResult::Message( "Successfully loaded identity".to_string(), )) } + + pub(super) async fn load_user_identities_up_to_index( + &self, + sdk: &Sdk, + wallet_arc_ref: WalletArcRef, + max_identity_index: IdentityIndex, + sender: crate::utils::egui_mpsc::SenderAsync, + ) -> Result { + let wallet_ref = wallet_arc_ref; + + let mut loaded_indices = Vec::new(); + let mut missing_indices = Vec::new(); + + for identity_index in 0..=max_identity_index { + match self + .load_user_identity_from_wallet( + sdk, + wallet_ref.clone(), + identity_index, + sender.clone(), + ) + .await + { + Ok(_) => { + loaded_indices.push(identity_index); + sender + .send(TaskResult::Success(Box::new( + BackendTaskSuccessResult::Message(format!( + "Loaded identity at index {}.", + identity_index + )), + ))) + .await + .map_err(|e| e.to_string())?; + } + Err(error) => { + if error.starts_with("No identity found for wallet identity index") { + missing_indices.push(identity_index); + sender + .send(TaskResult::Success(Box::new( + BackendTaskSuccessResult::Message(format!( + "No identity found at index {}.", + identity_index + )), + ))) + .await + .map_err(|e| e.to_string())?; + } else { + return Err(error); + } + } + } + } + + if loaded_indices.is_empty() { + return Err(format!( + "No identities found up to index {}.", + max_identity_index + )); + } + + let summary = if missing_indices.is_empty() { + format!( + "Successfully loaded {} identit{} up to index {}.", + loaded_indices.len(), + if loaded_indices.len() == 1 { + "y" + } else { + "ies" + }, + max_identity_index + ) + } else { + let missing_display = missing_indices + .iter() + .map(|idx| idx.to_string()) + .collect::>() + .join(", "); + format!( + "Finished loading identities up to index {}. Loaded {} identit{}; no identity found at index(es): {}.", + max_identity_index, + loaded_indices.len(), + if loaded_indices.len() == 1 { + "y" + } else { + "ies" + }, + missing_display + ) + }; + + Ok(BackendTaskSuccessResult::Message(summary)) + } } diff --git a/src/backend_task/identity/mod.rs b/src/backend_task/identity/mod.rs index aee36f4b7..ddf53bd97 100644 --- a/src/backend_task/identity/mod.rs +++ b/src/backend_task/identity/mod.rs @@ -17,7 +17,6 @@ use crate::model::qualified_identity::qualified_identity_public_key::QualifiedId use crate::model::qualified_identity::{IdentityType, PrivateKeyTarget, QualifiedIdentity}; use crate::model::wallet::{Wallet, WalletArcRef, WalletSeedHash}; use dash_sdk::Sdk; -use dash_sdk::dashcore_rpc::dashcore::bip32::DerivationPath; use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; use dash_sdk::dashcore_rpc::dashcore::{Address, PrivateKey, TxOut}; use dash_sdk::dpp::ProtocolError; @@ -29,6 +28,7 @@ use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dash_sdk::dpp::identity::identity_public_key::v0::IdentityPublicKeyV0; use dash_sdk::dpp::identity::{KeyID, KeyType, Purpose, SecurityLevel}; +use dash_sdk::dpp::key_wallet::bip32::DerivationPath; use dash_sdk::dpp::prelude::AssetLockProof; use dash_sdk::platform::{Identifier, Identity, IdentityPublicKey}; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -249,6 +249,7 @@ pub enum IdentityTask { LoadIdentity(IdentityInputToLoad), #[allow(dead_code)] // May be used for finding identities in wallets SearchIdentityFromWallet(WalletArcRef, IdentityIndex), + SearchIdentitiesUpToIndex(WalletArcRef, IdentityIndex), RegisterIdentity(IdentityRegistrationInfo), TopUpIdentity(IdentityTopUpInfo), AddKeyToIdentity(QualifiedIdentity, QualifiedIdentityPublicKey, [u8; 32]), @@ -452,7 +453,7 @@ impl AppContext { .await } IdentityTask::RegisterIdentity(registration_info) => { - self.register_identity(registration_info, sender).await + self.register_identity(registration_info).await } IdentityTask::RegisterDpnsName(input) => self.register_dpns_name(sdk, input).await, IdentityTask::RefreshIdentity(qualified_identity) => self @@ -464,12 +465,14 @@ impl AppContext { .await } IdentityTask::SearchIdentityFromWallet(wallet, identity_index) => { - self.load_user_identity_from_wallet(sdk, wallet, identity_index) + self.load_user_identity_from_wallet(sdk, wallet, identity_index, sender) .await } - IdentityTask::TopUpIdentity(top_up_info) => { - self.top_up_identity(top_up_info, sender).await + IdentityTask::SearchIdentitiesUpToIndex(wallet, max_identity_index) => { + self.load_user_identities_up_to_index(sdk, wallet, max_identity_index, sender) + .await } + IdentityTask::TopUpIdentity(top_up_info) => self.top_up_identity(top_up_info).await, IdentityTask::RefreshLoadedIdentitiesOwnedDPNSNames => { self.refresh_loaded_identities_dpns_names(sender).await } diff --git a/src/backend_task/identity/register_dpns_name.rs b/src/backend_task/identity/register_dpns_name.rs index d7ff0c963..888b2ae25 100644 --- a/src/backend_task/identity/register_dpns_name.rs +++ b/src/backend_task/identity/register_dpns_name.rs @@ -60,6 +60,7 @@ impl AppContext { let preorder_document = Document::V0(DocumentV0 { id: preorder_id, owner_id: qualified_identity.identity.id(), + creator_id: None, properties: BTreeMap::from([( "saltedDomainHash".to_string(), salted_domain_hash.into(), @@ -78,6 +79,7 @@ impl AppContext { let domain_document = Document::V0(DocumentV0 { id: domain_id, owner_id: qualified_identity.identity.id(), + creator_id: None, properties: BTreeMap::from([ ("parentDomainName".to_string(), "dash".into()), ("normalizedParentDomainName".to_string(), "dash".into()), diff --git a/src/backend_task/identity/register_identity.rs b/src/backend_task/identity/register_identity.rs index 57dc94c04..a3505ecce 100644 --- a/src/backend_task/identity/register_identity.rs +++ b/src/backend_task/identity/register_identity.rs @@ -1,4 +1,3 @@ -use crate::app::TaskResult; use crate::backend_task::BackendTaskSuccessResult; use crate::backend_task::identity::{IdentityRegistrationInfo, RegisterIdentityFundingMethod}; use crate::context::AppContext; @@ -20,93 +19,9 @@ use std::collections::BTreeMap; use std::time::Duration; impl AppContext { - // pub(crate) async fn broadcast_and_retrieve_asset_lock( - // &self, - // asset_lock_transaction: &Transaction, - // address: &Address, - // ) -> Result { - // // Use the span only for synchronous logging before the first await. - // // tracing::debug_span!( - // // "broadcast_and_retrieve_asset_lock", - // // transaction_id = asset_lock_transaction.txid().to_string(), - // // ) - // // .in_scope(|| { - // // tracing::debug!("Starting asset lock broadcast."); - // // }); - // - // let sdk = &self.sdk; - // - // let block_hash = sdk - // .execute(GetBlockchainStatusRequest {}, RequestSettings::default()) - // .await? - // .chain - // .map(|chain| chain.best_block_hash) - // .ok_or_else(|| dash_sdk::Error::DapiClientError("Missing `chain` field".to_owned()))?; - // - // // tracing::debug!( - // // "Starting the stream from the tip block hash {}", - // // hex::encode(&block_hash) - // // ); - // - // let mut asset_lock_stream = sdk - // .start_instant_send_lock_stream(block_hash, address) - // .await?; - // - // // tracing::debug!("Stream is started."); - // - // let request = BroadcastTransactionRequest { - // transaction: asset_lock_transaction.serialize(), - // allow_high_fees: false, - // bypass_limits: false, - // }; - // - // // tracing::debug!("Broadcasting the transaction."); - // - // match sdk.execute(request, RequestSettings::default()).await { - // Ok(_) => {} - // Err(error) if error.to_string().contains("AlreadyExists") => { - // // tracing::warn!("Transaction already broadcasted."); - // - // let GetTransactionResponse { block_hash, .. } = sdk - // .execute( - // GetTransactionRequest { - // id: asset_lock_transaction.txid().to_string(), - // }, - // RequestSettings::default(), - // ) - // .await?; - // - // // tracing::debug!( - // // "Restarting the stream from the transaction mined block hash {}", - // // hex::encode(&block_hash) - // // ); - // - // asset_lock_stream = sdk - // .start_instant_send_lock_stream(block_hash, address) - // .await?; - // - // // tracing::debug!("Stream restarted."); - // } - // Err(error) => { - // // tracing::error!("Transaction broadcast failed: {error}"); - // return Err(error.into()); - // } - // } - // - // // tracing::debug!("Waiting for asset lock proof."); - // - // sdk.wait_for_asset_lock_proof_for_transaction( - // asset_lock_stream, - // asset_lock_transaction, - // Some(Duration::from_secs(4 * 60)), - // ) - // .await - // } - pub(super) async fn register_identity( &self, input: IdentityRegistrationInfo, - sender: crate::utils::egui_mpsc::SenderAsync, ) -> Result { let IdentityRegistrationInfo { alias_input, @@ -203,12 +118,6 @@ impl AppContext { }; let tx_id = asset_lock_transaction.txid(); - // todo: maybe one day we will want to use platform again, but for right now we use - // the local core as it is more stable - // let asset_lock_proof = self - // .broadcast_and_retrieve_asset_lock(&asset_lock_transaction, &change_address) - // .await - // .map_err(|e| e.to_string())?; { let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); @@ -270,12 +179,6 @@ impl AppContext { }; let tx_id = asset_lock_transaction.txid(); - // todo: maybe one day we will want to use platform again, but for right now we use - // the local core as it is more stable - // let asset_lock_proof = self - // .broadcast_and_retrieve_asset_lock(&asset_lock_transaction, &change_address) - // .await - // .map_err(|e| e.to_string())?; { let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); @@ -322,14 +225,15 @@ impl AppContext { let public_keys = keys.to_public_keys_map(); - match Identity::fetch_by_identifier(&sdk, identity_id).await { - Ok(Some(_)) => return Err("Identity already exists".to_string()), - Ok(None) => {} + let existing_identity = match Identity::fetch_by_identifier(&sdk, identity_id).await { + Ok(result) => result, Err(e) => return Err(format!("Error fetching identity: {}", e)), }; - let identity = Identity::new_with_id_and_keys(identity_id, public_keys, sdk.version()) - .expect("expected to make identity"); + let identity = existing_identity.clone().unwrap_or_else(|| { + Identity::new_with_id_and_keys(identity_id, public_keys, sdk.version()) + .expect("expected to make identity") + }); let wallet_seed_hash = { wallet.read().unwrap().seed_hash() }; let mut qualified_identity = QualifiedIdentity { @@ -348,12 +252,42 @@ impl AppContext { wallet_index: Some(wallet_identity_index), top_ups: Default::default(), status: IdentityStatus::PendingCreation, + network: self.network, }; if !alias_input.is_empty() { qualified_identity.alias = Some(alias_input); } + if let Some(existing_identity) = existing_identity { + qualified_identity.identity = existing_identity; + qualified_identity.status = IdentityStatus::Unknown; + + self.insert_local_qualified_identity( + &qualified_identity, + &Some((wallet_id, wallet_identity_index)), + ) + .map_err(|e| e.to_string())?; + + { + let mut wallet = wallet.write().unwrap(); + wallet + .unused_asset_locks + .retain(|(tx, _, _, _, _)| tx.txid() != tx_id); + wallet + .identities + .insert(wallet_identity_index, qualified_identity.identity.clone()); + } + + self.db + .set_asset_lock_identity_id(tx_id.as_byte_array(), identity_id.as_bytes()) + .map_err(|e| e.to_string())?; + + return Ok(BackendTaskSuccessResult::RegisteredIdentity( + qualified_identity, + )); + } + self.insert_local_qualified_identity( &qualified_identity, &Some((wallet_id, wallet_identity_index)), @@ -413,13 +347,6 @@ impl AppContext { .set_asset_lock_identity_id(tx_id.as_byte_array(), identity_id.as_bytes()) .map_err(|e| e.to_string())?; - sender - .send(TaskResult::Success(Box::new( - BackendTaskSuccessResult::None, - ))) - .await - .map_err(|e| e.to_string())?; - Ok(BackendTaskSuccessResult::RegisteredIdentity( qualified_identity, )) diff --git a/src/backend_task/identity/top_up_identity.rs b/src/backend_task/identity/top_up_identity.rs index b2bb94808..3743b4d47 100644 --- a/src/backend_task/identity/top_up_identity.rs +++ b/src/backend_task/identity/top_up_identity.rs @@ -1,4 +1,3 @@ -use crate::app::TaskResult; use crate::backend_task::BackendTaskSuccessResult; use crate::backend_task::identity::{IdentityTopUpInfo, TopUpIdentityFundingMethod}; use crate::context::AppContext; @@ -21,7 +20,6 @@ impl AppContext { pub(super) async fn top_up_identity( &self, input: IdentityTopUpInfo, - sender: crate::utils::egui_mpsc::SenderAsync, ) -> Result { let IdentityTopUpInfo { mut qualified_identity, @@ -331,13 +329,6 @@ impl AppContext { .map_err(|e| e.to_string())?; } - sender - .send(TaskResult::Success(Box::new( - BackendTaskSuccessResult::None, - ))) - .await - .map_err(|e| e.to_string())?; - Ok(BackendTaskSuccessResult::ToppedUpIdentity( qualified_identity, )) diff --git a/src/backend_task/mnlist.rs b/src/backend_task/mnlist.rs new file mode 100644 index 000000000..4dfd2995a --- /dev/null +++ b/src/backend_task/mnlist.rs @@ -0,0 +1,128 @@ +use crate::backend_task::BackendTaskSuccessResult; +use crate::components::core_p2p_handler::CoreP2PHandler; +use crate::context::AppContext; +use dash_sdk::dashcore_rpc::RpcApi; +use dash_sdk::dpp::dashcore::bls_sig_utils::BLSSignature; +use dash_sdk::dpp::dashcore::hashes::Hash; +use dash_sdk::dpp::dashcore::{BlockHash, Network}; + +#[derive(Debug, Clone, PartialEq)] +pub enum MnListTask { + FetchEndDmlDiff { + base_block_height: u32, + base_block_hash: BlockHash, + block_height: u32, + block_hash: BlockHash, + validate_quorums: bool, + }, + FetchEndQrInfo { + known_block_hashes: Vec, + block_hash: BlockHash, + }, + FetchEndQrInfoWithDmls { + known_block_hashes: Vec, + block_hash: BlockHash, + }, + FetchChainLocks { + base_block_height: u32, + block_height: u32, + }, + /// Fetch a sequence of MNListDiffs for validation purposes + /// Each tuple is (base_height, base_hash, height, hash) + FetchDiffsChain { + chain: Vec<(u32, BlockHash, u32, BlockHash)>, + }, +} + +pub async fn run_mnlist_task( + app: &AppContext, + task: MnListTask, +) -> Result { + match task { + MnListTask::FetchEndDmlDiff { + base_block_height, + base_block_hash, + block_height, + block_hash, + validate_quorums: _, + } => { + let network = app.network; + let mut p2p = CoreP2PHandler::new(network, None)?; + let diff = p2p.get_dml_diff(base_block_hash, block_hash)?; + Ok(BackendTaskSuccessResult::MnListFetchedDiff { + base_height: base_block_height, + height: block_height, + diff, + }) + } + MnListTask::FetchEndQrInfo { + known_block_hashes, + block_hash, + } => { + let network = app.network; + let mut p2p = CoreP2PHandler::new(network, None)?; + let qr_info = p2p.get_qr_info(known_block_hashes, block_hash)?; + Ok(BackendTaskSuccessResult::MnListFetchedQrInfo { qr_info }) + } + MnListTask::FetchEndQrInfoWithDmls { + known_block_hashes, + block_hash, + } => { + // For now, fetch QRInfo; UI can integrate included diffs from QRInfo + let network = app.network; + let mut p2p = CoreP2PHandler::new(network, None)?; + let qr_info = p2p.get_qr_info(known_block_hashes, block_hash)?; + Ok(BackendTaskSuccessResult::MnListFetchedQrInfo { qr_info }) + } + MnListTask::FetchChainLocks { + base_block_height, + block_height, + } => { + let client = app.core_client.read().unwrap(); + // Determine the range (replicate UI logic approximately) + let loaded_list_height = match app.network { + Network::Dash => 2_227_096, + Network::Testnet => 1_296_600, + _ => 0, + }; + let max_blocks = 2000u32; + let start_height = if base_block_height < loaded_list_height { + block_height.saturating_sub(max_blocks) + } else { + base_block_height + }; + let end_height = start_height.saturating_add(max_blocks).min(block_height); + + let mut out: Vec<((u32, BlockHash), Option)> = Vec::new(); + for h in start_height..end_height { + if let Ok(bh2) = client.get_block_hash(h) { + // Convert RPC hash to DPP hash + let bh = BlockHash::from_byte_array(bh2.to_byte_array()); + // Get block and extract coinbase best_cl_signature + if let Ok(block) = client.get_block(&bh2) { + let sig_opt = block + .coinbase() + .and_then(|cb| cb.special_transaction_payload.as_ref()) + .and_then(|pl| pl.clone().to_coinbase_payload().ok()) + .and_then(|cp| cp.best_cl_signature) + .map(|sig| sig.to_bytes().into()); + out.push(((h, bh), sig_opt)); + } else { + out.push(((h, bh), None)); + } + } + } + Ok(BackendTaskSuccessResult::MnListChainLockSigs { entries: out }) + } + MnListTask::FetchDiffsChain { chain } => { + let network = app.network; + let mut p2p = CoreP2PHandler::new(network, None)?; + let mut items = Vec::with_capacity(chain.len()); + for (base_h, base_hash, h, hash) in chain { + let diff = p2p.get_dml_diff(base_hash, hash)?; + items.push(((base_h, h), diff)); + } + Ok(BackendTaskSuccessResult::MnListFetchedDiffs { items }) + } + } +} diff --git a/src/backend_task/mod.rs b/src/backend_task/mod.rs index 1c5eb7299..32b801e36 100644 --- a/src/backend_task/mod.rs +++ b/src/backend_task/mod.rs @@ -7,17 +7,23 @@ use crate::backend_task::identity::IdentityTask; use crate::backend_task::platform_info::{PlatformInfoTaskRequestType, PlatformInfoTaskResult}; use crate::backend_task::system_task::SystemTask; use crate::context::AppContext; +use dash_sdk::dpp::dashcore::bls_sig_utils::BLSSignature; +use dash_sdk::dpp::dashcore::network::message_qrinfo::QRInfo; +use dash_sdk::dpp::dashcore::BlockHash; use crate::model::qualified_identity::QualifiedIdentity; +use crate::model::grovestark_prover::ProofDataOutput; use crate::ui::tokens::tokens_screen::{ ContractDescriptionInfo, IdentityTokenIdentifier, TokenInfo, }; use crate::utils::egui_mpsc::SenderAsync; use contested_names::ScheduledDPNSVote; use dash_sdk::dpp::balances::credits::TokenAmount; +use dash_sdk::dpp::dashcore::network::message_sml::MnListDiff; use dash_sdk::dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::evaluate_interval::IntervalEvaluationExplanation; use dash_sdk::dpp::group::group_action::GroupAction; use dash_sdk::dpp::prelude::DataContract; use dash_sdk::dpp::state_transition::StateTransition; +use dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use dash_sdk::dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice; use dash_sdk::dpp::voting::votes::Vote; use dash_sdk::platform::proto::get_documents_request::get_documents_request_v0::Start; @@ -27,13 +33,16 @@ use futures::future::join_all; use std::collections::BTreeMap; use std::sync::Arc; use tokens::TokenTask; +use grovestark::GroveSTARKTask; pub mod broadcast_state_transition; pub mod contested_names; pub mod contract; pub mod core; pub mod document; +pub mod grovestark; pub mod identity; +pub mod mnlist; pub mod platform_info; pub mod register_contract; pub mod system_task; @@ -53,7 +62,9 @@ pub enum BackendTask { BroadcastStateTransition(StateTransition), TokenTask(Box), SystemTask(SystemTask), + MnListTask(mnlist::MnListTask), PlatformInfo(PlatformInfoTaskRequestType), + GroveSTARKTask(GroveSTARKTask), None, } @@ -95,10 +106,27 @@ pub enum BackendTaskSuccessResult { ActiveGroupActions(IndexMap), TokenPricing { token_id: Identifier, - prices: Option, + prices: Option, }, UpdatedThemePreference(crate::ui::theme::ThemeMode), PlatformInfo(PlatformInfoTaskResult), + GeneratedZKProof(ProofDataOutput), + VerifiedZKProof(bool, ProofDataOutput), + // MNList-specific results + MnListFetchedDiff { + base_height: u32, + height: u32, + diff: MnListDiff, + }, + MnListFetchedQrInfo { + qr_info: QRInfo, + }, + MnListChainLockSigs { + entries: Vec<((u32, BlockHash), Option)>, + }, + MnListFetchedDiffs { + items: Vec<((u32, u32), MnListDiff)>, + }, } impl BackendTaskSuccessResult {} @@ -171,9 +199,15 @@ impl AppContext { self.run_token_task(*token_task, &sdk, sender).await } BackendTask::SystemTask(system_task) => self.run_system_task(system_task, sender).await, + BackendTask::MnListTask(mnlist_task) => { + mnlist::run_mnlist_task(self, mnlist_task).await + } BackendTask::PlatformInfo(platform_info_task) => { self.run_platform_info_task(platform_info_task).await } + BackendTask::GroveSTARKTask(grovestark_task) => { + grovestark::run_grovestark_task(grovestark_task, &sdk).await + } BackendTask::None => Ok(BackendTaskSuccessResult::None), } } diff --git a/src/backend_task/system_task/mod.rs b/src/backend_task/system_task/mod.rs index cba2e729d..2999b43fb 100644 --- a/src/backend_task/system_task/mod.rs +++ b/src/backend_task/system_task/mod.rs @@ -48,6 +48,8 @@ impl AppContext { self: &Arc, theme_mode: ThemeMode, ) -> Result { + let _guard = self.invalidate_settings_cache(); + self.db .update_theme_preference(theme_mode) .map_err(|e| e.to_string())?; diff --git a/src/backend_task/tokens/burn_tokens.rs b/src/backend_task/tokens/burn_tokens.rs index c50ba5c26..bb7163056 100644 --- a/src/backend_task/tokens/burn_tokens.rs +++ b/src/backend_task/tokens/burn_tokens.rs @@ -89,21 +89,16 @@ impl AppContext { BurnResult::HistoricalDocument(document) => { if let (Some(owner_value), Some(amount_value)) = (document.get("ownerId"), document.get("amount")) - { - if let (Value::Identifier(owner_bytes), Value::U64(amount)) = + && let (Value::Identifier(owner_bytes), Value::U64(amount)) = (owner_value, amount_value) - { - if let Ok(owner_id) = Identifier::from_bytes(owner_bytes) { - if let Err(e) = self - .insert_token_identity_balance(&token_id, &owner_id, *amount) - { - eprintln!( - "Failed to update token balance from historical document: {}", - e - ); - } - } - } + && let Ok(owner_id) = Identifier::from_bytes(owner_bytes) + && let Err(e) = + self.insert_token_identity_balance(&token_id, &owner_id, *amount) + { + eprintln!( + "Failed to update token balance from historical document: {}", + e + ); } } @@ -111,21 +106,16 @@ impl AppContext { BurnResult::GroupActionWithDocument(_, Some(document)) => { if let (Some(owner_value), Some(amount_value)) = (document.get("ownerId"), document.get("amount")) - { - if let (Value::Identifier(owner_bytes), Value::U64(amount)) = + && let (Value::Identifier(owner_bytes), Value::U64(amount)) = (owner_value, amount_value) - { - if let Ok(owner_id) = Identifier::from_bytes(owner_bytes) { - if let Err(e) = self - .insert_token_identity_balance(&token_id, &owner_id, *amount) - { - eprintln!( - "Failed to update token balance from group action document: {}", - e - ); - } - } - } + && let Ok(owner_id) = Identifier::from_bytes(owner_bytes) + && let Err(e) = + self.insert_token_identity_balance(&token_id, &owner_id, *amount) + { + eprintln!( + "Failed to update token balance from group action document: {}", + e + ); } } diff --git a/src/backend_task/tokens/claim_tokens.rs b/src/backend_task/tokens/claim_tokens.rs index b3a3b8473..ec22b2fff 100644 --- a/src/backend_task/tokens/claim_tokens.rs +++ b/src/backend_task/tokens/claim_tokens.rs @@ -73,23 +73,13 @@ impl AppContext { ClaimResult::Document(document) => { if let (Some(claimer_value), Some(amount_value)) = (document.get("claimerId"), document.get("amount")) - { - if let (Value::Identifier(claimer_bytes), Value::U64(amount)) = + && let (Value::Identifier(claimer_bytes), Value::U64(amount)) = (claimer_value, amount_value) - { - if let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) { - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &claimer_id, - *amount, - ) { - eprintln!( - "Failed to update token balance from claim document: {}", - e - ); - } - } - } + && let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) + && let Err(e) = + self.insert_token_identity_balance(&token_id, &claimer_id, *amount) + { + eprintln!("Failed to update token balance from claim document: {}", e); } } @@ -97,23 +87,16 @@ impl AppContext { ClaimResult::GroupActionWithDocument(_, document) => { if let (Some(claimer_value), Some(amount_value)) = (document.get("claimerId"), document.get("amount")) - { - if let (Value::Identifier(claimer_bytes), Value::U64(amount)) = + && let (Value::Identifier(claimer_bytes), Value::U64(amount)) = (claimer_value, amount_value) - { - if let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) { - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &claimer_id, - *amount, - ) { - eprintln!( - "Failed to update token balance from group action document: {}", - e - ); - } - } - } + && let Ok(claimer_id) = Identifier::from_bytes(claimer_bytes) + && let Err(e) = + self.insert_token_identity_balance(&token_id, &claimer_id, *amount) + { + eprintln!( + "Failed to update token balance from group action document: {}", + e + ); } } } diff --git a/src/backend_task/tokens/mint_tokens.rs b/src/backend_task/tokens/mint_tokens.rs index f23ee1142..86a806745 100644 --- a/src/backend_task/tokens/mint_tokens.rs +++ b/src/backend_task/tokens/mint_tokens.rs @@ -96,23 +96,16 @@ impl AppContext { MintResult::HistoricalDocument(document) => { if let (Some(recipient_value), Some(amount_value)) = (document.get("recipientId"), document.get("amount")) - { - if let (Value::Identifier(recipient_bytes), Value::U64(amount)) = + && let (Value::Identifier(recipient_bytes), Value::U64(amount)) = (recipient_value, amount_value) - { - if let Ok(recipient_id) = Identifier::from_bytes(recipient_bytes) { - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &recipient_id, - *amount, - ) { - eprintln!( - "Failed to update token balance from historical document: {}", - e - ); - } - } - } + && let Ok(recipient_id) = Identifier::from_bytes(recipient_bytes) + && let Err(e) = + self.insert_token_identity_balance(&token_id, &recipient_id, *amount) + { + eprintln!( + "Failed to update token balance from historical document: {}", + e + ); } } @@ -120,23 +113,16 @@ impl AppContext { MintResult::GroupActionWithDocument(_, Some(document)) => { if let (Some(recipient_value), Some(amount_value)) = (document.get("recipientId"), document.get("amount")) - { - if let (Value::Identifier(recipient_bytes), Value::U64(amount)) = + && let (Value::Identifier(recipient_bytes), Value::U64(amount)) = (recipient_value, amount_value) - { - if let Ok(recipient_id) = Identifier::from_bytes(recipient_bytes) { - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &recipient_id, - *amount, - ) { - eprintln!( - "Failed to update token balance from group action document: {}", - e - ); - } - } - } + && let Ok(recipient_id) = Identifier::from_bytes(recipient_bytes) + && let Err(e) = + self.insert_token_identity_balance(&token_id, &recipient_id, *amount) + { + eprintln!( + "Failed to update token balance from group action document: {}", + e + ); } } diff --git a/src/backend_task/tokens/mod.rs b/src/backend_task/tokens/mod.rs index 40a1c8b80..aa8c40ddb 100644 --- a/src/backend_task/tokens/mod.rs +++ b/src/backend_task/tokens/mod.rs @@ -720,6 +720,8 @@ impl AppContext { let mut validation_operations = Vec::new(); match dash_sdk::dpp::data_contract::document_type::DocumentType::try_from_schema( contract_id, + 0, + 0, &name, platform_value, None, // schema_defs diff --git a/src/backend_task/tokens/query_tokens.rs b/src/backend_task/tokens/query_tokens.rs index 1ea3e8d46..014c57400 100644 --- a/src/backend_task/tokens/query_tokens.rs +++ b/src/backend_task/tokens/query_tokens.rs @@ -1,5 +1,9 @@ //! Execute token query by keyword on Platform +use crate::{ + backend_task::BackendTaskSuccessResult, context::AppContext, + ui::tokens::tokens_screen::ContractDescriptionInfo, +}; use dash_sdk::{ Sdk, dpp::{document::DocumentV0Getters, platform_value::Value}, @@ -10,11 +14,6 @@ use dash_sdk::{ }, }; -use crate::{ - backend_task::BackendTaskSuccessResult, context::AppContext, - ui::tokens::tokens_screen::ContractDescriptionInfo, -}; - impl AppContext { /// 1. Fetch all **contractKeywords** docs that match `keyword` from the Search Contract /// 2. For every `contractId` found, fetch its **shortDescription** document from the Search Contract @@ -39,19 +38,15 @@ impl AppContext { let kw_docs = Document::fetch_many(sdk, kw_query.clone()) .await - .map_err(|e| format!("Error fetching keyword docs: {e}"))?; + .map_err(|e| e.to_string())?; // store the order for deterministic pagination let mut contract_ids: Vec = Vec::with_capacity(kw_docs.len()); for (_doc_id, doc_opt) in kw_docs.iter() { - if let Some(doc) = doc_opt { - if let Some(cid_val) = doc.get("contractId") { - contract_ids.push( - cid_val - .to_identifier() - .map_err(|e| format!("Bad contractId: {e}"))?, - ); - } + if let Some(doc) = doc_opt + && let Some(cid_val) = doc.get("contractId") + { + contract_ids.push(cid_val.to_identifier().map_err(|e| e.to_string())?); } } @@ -85,7 +80,7 @@ impl AppContext { let description = if let Some((_, Some(desc_doc))) = Document::fetch_many(sdk, desc_query) .await - .map_err(|e| format!("Error fetching description doc: {e}"))? + .map_err(|e| e.to_string())? .into_iter() .next() { diff --git a/src/backend_task/tokens/set_token_price.rs b/src/backend_task/tokens/set_token_price.rs index eb5222ee1..7b8643e2e 100644 --- a/src/backend_task/tokens/set_token_price.rs +++ b/src/backend_task/tokens/set_token_price.rs @@ -31,9 +31,12 @@ impl AppContext { data_contract.clone(), token_position, sending_identity.identity.id(), - token_pricing_schedule, ); + if let Some(pricing_schedule) = token_pricing_schedule { + builder = builder.with_token_pricing_schedule(pricing_schedule); + } + if let Some(note) = public_note { builder = builder.with_public_note(note); } diff --git a/src/backend_task/tokens/transfer_tokens.rs b/src/backend_task/tokens/transfer_tokens.rs index 71e5d232c..93a7a2b8e 100644 --- a/src/backend_task/tokens/transfer_tokens.rs +++ b/src/backend_task/tokens/transfer_tokens.rs @@ -99,43 +99,39 @@ impl AppContext { document.get("senderAmount"), document.get("recipientId"), document.get("recipientAmount"), + ) && let ( + Value::Identifier(sender_bytes), + Value::U64(sender_amount), + Value::Identifier(recipient_bytes), + Value::U64(recipient_amount), + ) = ( + sender_value, + sender_amount_value, + recipient_value, + recipient_amount_value, + ) && let (Ok(sender_id), Ok(recipient_id)) = ( + Identifier::from_bytes(sender_bytes), + Identifier::from_bytes(recipient_bytes), ) { - if let ( - Value::Identifier(sender_bytes), - Value::U64(sender_amount), - Value::Identifier(recipient_bytes), - Value::U64(recipient_amount), - ) = ( - sender_value, - sender_amount_value, - recipient_value, - recipient_amount_value, + if let Err(e) = self.insert_token_identity_balance( + &token_id, + &sender_id, + *sender_amount, ) { - if let (Ok(sender_id), Ok(recipient_id)) = ( - Identifier::from_bytes(sender_bytes), - Identifier::from_bytes(recipient_bytes), - ) { - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &sender_id, - *sender_amount, - ) { - eprintln!( - "Failed to update sender token balance from historical document: {}", - e - ); - } - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &recipient_id, - *recipient_amount, - ) { - eprintln!( - "Failed to update recipient token balance from historical document: {}", - e - ); - } - } + eprintln!( + "Failed to update sender token balance from historical document: {}", + e + ); + } + if let Err(e) = self.insert_token_identity_balance( + &token_id, + &recipient_id, + *recipient_amount, + ) { + eprintln!( + "Failed to update recipient token balance from historical document: {}", + e + ); } } } @@ -152,43 +148,39 @@ impl AppContext { document.get("senderAmount"), document.get("recipientId"), document.get("recipientAmount"), + ) && let ( + Value::Identifier(sender_bytes), + Value::U64(sender_amount), + Value::Identifier(recipient_bytes), + Value::U64(recipient_amount), + ) = ( + sender_value, + sender_amount_value, + recipient_value, + recipient_amount_value, + ) && let (Ok(sender_id), Ok(recipient_id)) = ( + Identifier::from_bytes(sender_bytes), + Identifier::from_bytes(recipient_bytes), ) { - if let ( - Value::Identifier(sender_bytes), - Value::U64(sender_amount), - Value::Identifier(recipient_bytes), - Value::U64(recipient_amount), - ) = ( - sender_value, - sender_amount_value, - recipient_value, - recipient_amount_value, + if let Err(e) = self.insert_token_identity_balance( + &token_id, + &sender_id, + *sender_amount, ) { - if let (Ok(sender_id), Ok(recipient_id)) = ( - Identifier::from_bytes(sender_bytes), - Identifier::from_bytes(recipient_bytes), - ) { - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &sender_id, - *sender_amount, - ) { - eprintln!( - "Failed to update sender token balance from group action document: {}", - e - ); - } - if let Err(e) = self.insert_token_identity_balance( - &token_id, - &recipient_id, - *recipient_amount, - ) { - eprintln!( - "Failed to update recipient token balance from group action document: {}", - e - ); - } - } + eprintln!( + "Failed to update sender token balance from group action document: {}", + e + ); + } + if let Err(e) = self.insert_token_identity_balance( + &token_id, + &recipient_id, + *recipient_amount, + ) { + eprintln!( + "Failed to update recipient token balance from group action document: {}", + e + ); } } } diff --git a/src/components/core_p2p_handler.rs b/src/components/core_p2p_handler.rs new file mode 100644 index 000000000..6b354598b --- /dev/null +++ b/src/components/core_p2p_handler.rs @@ -0,0 +1,471 @@ +use chrono::Utc; +use dash_sdk::dpp::dashcore::BlockHash; +use dash_sdk::dpp::dashcore::Network; +use dash_sdk::dpp::dashcore::consensus::{deserialize, serialize}; +use dash_sdk::dpp::dashcore::network::constants::ServiceFlags; +use dash_sdk::dpp::dashcore::network::message::{NetworkMessage, RawNetworkMessage}; +use dash_sdk::dpp::dashcore::network::message_qrinfo::QRInfo; +use dash_sdk::dpp::dashcore::network::message_sml::{GetMnListDiff, MnListDiff}; +use dash_sdk::dpp::dashcore::network::{Address, message_network, message_qrinfo}; +use rand::prelude::StdRng; +use rand::{Rng, SeedableRng}; +use sha2::{Digest, Sha256}; +use std::io::{ErrorKind, Read, Write}; +use std::net::TcpStream; +use std::thread; +use std::time::Duration; + +#[derive(Debug)] +pub struct CoreP2PHandler { + pub network: Network, + pub port: u16, + pub stream: TcpStream, + pub handshake_success: bool, +} + +/// Dash P2P header length in bytes +const HEADER_LENGTH: usize = 24; + +/// Maximum message payload size (e.g. 0x02000000 bytes) +const MAX_MSG_LENGTH: usize = 0x02000000; + +/// Compute double-SHA256 on the given data. +fn double_sha256(data: &[u8]) -> [u8; 32] { + let hash1 = Sha256::digest(data); + let hash2 = Sha256::digest(hash1); + let mut result = [0u8; 32]; + result.copy_from_slice(&hash2); + result +} + +#[derive(Debug)] +enum ReadMessageError { + Transient, + Fatal(String), +} + +impl CoreP2PHandler { + pub fn new(network: Network, use_port: Option) -> Result { + let port = use_port.unwrap_or(match network { + Network::Dash => 9999, // Dash Mainnet default + Network::Testnet => 19999, // Dash Testnet default + Network::Devnet => 29999, // Dash Devnet default + Network::Regtest => 29999, // Dash Regtest default + _ => panic!("Unsupported network type"), + }); + let stream = TcpStream::connect_timeout( + &format!("127.0.0.1:{}", port) + .parse() + .map_err(|e| format!("Invalid address: {}", e))?, + Duration::from_secs(5), + ) + .map_err(|e| format!("Failed to connect: {}", e))?; + // Set per-socket timeouts so reads/writes don't block forever + stream + .set_read_timeout(Some(Duration::from_secs(5))) + .map_err(|e| format!("set_read_timeout failed: {}", e))?; + stream + .set_write_timeout(Some(Duration::from_secs(5))) + .map_err(|e| format!("set_write_timeout failed: {}", e))?; + println!("Connected to Dash Core at 127.0.0.1:{}", port); + Ok(CoreP2PHandler { + network, + port, + stream, + handshake_success: false, + }) + } + + /// Sends a network message over the provided stream and waits for a response. + pub fn send_dml_request_message( + &mut self, + network_message: NetworkMessage, + ) -> Result { + if !self.handshake_success { + self.handshake()?; + } + let stream = &mut self.stream; + let raw_message = RawNetworkMessage { + magic: self.network.magic(), + payload: network_message, + }; + let encoded_message = serialize(&raw_message); + stream + .write_all(&encoded_message) + .map_err(|e| format!("Failed to send message: {}", e))?; + println!("Sent getmnlistdiff message to Dash Core"); + + let (mut command, mut payload); + let start_time = std::time::Instant::now(); + let timeout = Duration::from_secs(5); + loop { + if start_time.elapsed() > timeout { + return Err("Timeout waiting for mnlistdiff message".to_string()); + } + match self.read_message() { + Ok((c, p)) => { + command = c; + payload = p; + } + Err(ReadMessageError::Transient) => { + thread::sleep(Duration::from_millis(10)); + continue; + } + Err(ReadMessageError::Fatal(e)) => return Err(e), + } + if command == "mnlistdiff" { + println!("Got mnlistdiff message"); + break; + } else { + thread::sleep(Duration::from_millis(10)); + } + } + + // let log_file_path = app_user_data_file_path("DML.DAT").expect("should create DML.dat"); + // let mut log_file = match std::fs::File::create(log_file_path) { + // Ok(file) => file, + // Err(e) => panic!("Failed to create log file: {:?}", e), + // }; + // + // log_file.write_all(&payload).expect("expected to write"); + + let response_message: RawNetworkMessage = deserialize(&payload).map_err(|e| { + format!( + "Failed to deserialize response: {}, payload {}", + e, + hex::encode(payload) + ) + })?; + + match response_message.payload { + NetworkMessage::MnListDiff(diff) => Ok(diff), + network_message => Err(format!( + "Unexpected response type, expected MnListDiff, got {:?}", + network_message + )), + } + } + + /// Sends a network message over the provided stream and waits for a response. + pub fn send_qr_info_request_message( + &mut self, + network_message: NetworkMessage, + ) -> Result { + if !self.handshake_success { + self.handshake()?; + } + let stream = &mut self.stream; + let raw_message = RawNetworkMessage { + magic: self.network.magic(), + payload: network_message, + }; + let encoded_message = serialize(&raw_message); + stream + .write_all(&encoded_message) + .map_err(|e| format!("Failed to send message: {}", e))?; + println!("Sent qr info request message to Dash Core"); + + let (mut command, mut payload); + // QRInfo on mainnet can take noticeably longer to prepare. + // Temporarily increase socket read timeout and our overall wait. + let (socket_timeout, overall_timeout) = match self.network { + Network::Dash => (Duration::from_secs(60), Duration::from_secs(60)), + _ => (Duration::from_secs(15), Duration::from_secs(15)), + }; + let previous_socket_timeout = self + .stream + .read_timeout() + .map_err(|e| format!("get_read_timeout failed: {}", e))?; + self.stream + .set_read_timeout(Some(socket_timeout)) + .map_err(|e| format!("set_read_timeout failed: {}", e))?; + let start_time = std::time::Instant::now(); + let timeout = overall_timeout; + loop { + if start_time.elapsed() > timeout { + // Restore previous socket timeout before returning + self.stream + .set_read_timeout(previous_socket_timeout) + .map_err(|e| format!("restore set_read_timeout failed: {}", e))?; + return Err("Timeout waiting for qrinfo message".to_string()); + } + match self.read_message() { + Ok((c, p)) => { + command = c; + payload = p; + } + Err(ReadMessageError::Transient) => { + thread::sleep(Duration::from_millis(10)); + continue; + } + Err(ReadMessageError::Fatal(e)) => return Err(e), + } + if command == "qrinfo" { + println!("Got qrinfo message"); + // Restore previous socket timeout + self.stream + .set_read_timeout(previous_socket_timeout) + .map_err(|e| format!("restore set_read_timeout failed: {}", e))?; + break; + } else { + thread::sleep(Duration::from_millis(10)); + } + } + + // let log_file_path = app_user_data_file_path("QR_INFO.DAT").expect("should create DML.dat"); + // let mut log_file = match std::fs::File::create(log_file_path) { + // Ok(file) => file, + // Err(e) => panic!("Failed to create log file: {:?}", e), + // }; + // + // log_file.write_all(&payload).expect("expected to write"); + + let response_message: RawNetworkMessage = deserialize(&payload).map_err(|e| { + format!( + "Failed to deserialize response: {}, payload {}", + e, + hex::encode(payload) + ) + })?; + + match response_message.payload { + NetworkMessage::QRInfo(qr_info) => { + // let bytes = serialize(&qr_info); + // let log_file_path = app_user_data_file_path("QR_INFO.DAT").expect("should create DML.dat"); + // let mut log_file = match std::fs::File::create(log_file_path) { + // Ok(file) => file, + // Err(e) => panic!("Failed to create log file: {:?}", e), + // }; + // + // log_file.write_all(&bytes).expect("expected to write"); + Ok(qr_info) + } + network_message => Err(format!( + "Unexpected response type, expected QrInfo, got {:?}", + network_message + )), + } + } + + // Note: get_dml_diff and get_qr_info are already defined above (lines ~351 and ~364) + /// Perform the handshake (version/verack exchange) with the peer. + pub fn handshake(&mut self) -> Result<(), String> { + let mut rng = StdRng::from_entropy(); + + // Build a version message. + let version_msg = NetworkMessage::Version(message_network::VersionMessage { + version: 70235, + services: ServiceFlags::NONE, + timestamp: Utc::now().timestamp(), + receiver: Address { + services: ServiceFlags::BLOOM, + address: Default::default(), + port: self.stream.peer_addr().map_err(|e| e.to_string())?.port(), + }, + sender: Address { + services: ServiceFlags::NONE, + address: Default::default(), + port: self.stream.local_addr().map_err(|e| e.to_string())?.port(), + }, + nonce: rng.r#gen(), + user_agent: "/dash-evo-tool:0.9/".to_string(), + start_height: 0, + relay: false, + mn_auth_challenge: rng.r#gen(), + masternode_connection: false, + }); + + // Wrap it in a raw message. + let raw_version = RawNetworkMessage { + magic: self.network.magic(), + payload: version_msg, + }; + let encoded_version = serialize(&raw_version); + self.stream + .write_all(&encoded_version) + .map_err(|e| format!("Failed to send version: {}", e))?; + println!("Sent version message"); + + thread::sleep(Duration::from_millis(50)); + + // Read and process incoming messages until handshake is complete. + self.run_handshake_loop()?; + self.handshake_success = true; + Ok(()) + } + + fn read_message(&mut self) -> Result<(String, Vec), ReadMessageError> { + let mut header_buf = [0u8; HEADER_LENGTH]; + // Read the header. + self.stream + .read_exact(&mut header_buf) + .map_err(|e| match e.kind() { + ErrorKind::WouldBlock | ErrorKind::TimedOut => ReadMessageError::Transient, + _ => ReadMessageError::Fatal(format!("Error reading header: {}", e)), + })?; + + // If the first 4 bytes don't match our network magic, shift until we do. + const MAX_SYNC_ATTEMPTS: usize = 1024; // Prevent reading more than 1KB looking for magic + let mut sync_attempts = 0; + while u32::from_le_bytes(header_buf[0..4].try_into().unwrap()) != self.network.magic() { + sync_attempts += 1; + if sync_attempts > MAX_SYNC_ATTEMPTS { + return Err(ReadMessageError::Fatal( + "Failed to find network magic in stream".to_string(), + )); + } + // Shift left by one byte. + for i in 0..HEADER_LENGTH - 1 { + header_buf[i] = header_buf[i + 1]; + } + // Read one more byte. + let mut one_byte = [0u8; 1]; + self.stream + .read_exact(&mut one_byte) + .map_err(|e| match e.kind() { + ErrorKind::WouldBlock | ErrorKind::TimedOut => ReadMessageError::Transient, + _ => { + ReadMessageError::Fatal(format!("Error reading while syncing magic: {}", e)) + } + })?; + header_buf[HEADER_LENGTH - 1] = one_byte[0]; + } + + // Extract the command. + let command_bytes = &header_buf[4..16]; + let command = String::from_utf8_lossy(command_bytes) + .trim_matches('\0') + .to_string(); + + // Payload length (little-endian u32) + let payload_len_u32 = u32::from_le_bytes(header_buf[16..20].try_into().unwrap()); + if payload_len_u32 > MAX_MSG_LENGTH as u32 { + return Err(ReadMessageError::Fatal(format!( + "Payload length {} exceeds maximum", + payload_len_u32 + ))); + } + let payload_len = payload_len_u32 as usize; + + // Expected checksum. + let expected_checksum = &header_buf[20..24]; + + // Read the payload. + let mut payload_buf = vec![0u8; payload_len]; + self.stream + .read_exact(&mut payload_buf) + .map_err(|e| match e.kind() { + ErrorKind::WouldBlock | ErrorKind::TimedOut => ReadMessageError::Transient, + _ => ReadMessageError::Fatal(format!("Error reading payload: {}", e)), + })?; + + // Compute and verify checksum. + let computed_checksum = &double_sha256(&payload_buf)[0..4]; + if computed_checksum != expected_checksum { + return Err(ReadMessageError::Fatal(format!( + "Checksum mismatch for {}: computed {:x?}, expected {:x?}, payload is {:x?}", + command, computed_checksum, expected_checksum, payload_buf + ))); + } + let mut total_buf = header_buf.to_vec(); + total_buf.append(&mut payload_buf); + Ok((command, total_buf)) + } + + /// The handshake loop: read messages until we complete the version/verack exchange. + fn run_handshake_loop(&mut self) -> Result<(), String> { + // Expect a version message from the peer, with a timeout. + let start_time = std::time::Instant::now(); + let timeout = Duration::from_secs(5); + let (command, payload) = loop { + if start_time.elapsed() > timeout { + return Err("Timeout waiting for version message".to_string()); + } + match self.read_message() { + Ok(res) => break res, + Err(ReadMessageError::Transient) => { + thread::sleep(Duration::from_millis(10)); + continue; + } + Err(ReadMessageError::Fatal(e)) => return Err(e), + } + }; + if command != "version" { + return Err(format!("Expected version message, got {}", command)); + } + // Deserialize the version message payload. + let raw: RawNetworkMessage = deserialize(&payload) + .map_err(|e| format!("Failed to deserialize version payload: {}", e))?; + match raw.payload { + NetworkMessage::Version(peer_version) => { + println!("Received peer version: {:?}", peer_version); + } + _ => { + return Err("Deserialized message was not a version message".to_string()); + } + } + + let start_time = std::time::Instant::now(); + let timeout = Duration::from_secs(5); + loop { + if start_time.elapsed() > timeout { + return Err("Timeout waiting for verack message".to_string()); + } + let (command, _) = match self.read_message() { + Ok(res) => res, + Err(ReadMessageError::Transient) => { + thread::sleep(Duration::from_millis(10)); + continue; + } + Err(ReadMessageError::Fatal(e)) => return Err(e), + }; + if command == "verack" { + println!("Got verack message"); + break; + } else { + thread::sleep(Duration::from_millis(10)); + } + } + + // Send verack. + let verack_msg = NetworkMessage::Verack; + let raw_verack = RawNetworkMessage { + magic: self.network.magic(), + payload: verack_msg, + }; + let encoded_verack = serialize(&raw_verack); + self.stream + .write_all(&encoded_verack) + .map_err(|e| format!("Failed to send verack: {}", e))?; + + println!("Sent verack message"); + Ok(()) + } + + /// Sends a `GetMnListDiff` request after completing the handshake. + pub fn get_dml_diff( + &mut self, + base_block_hash: BlockHash, + block_hash: BlockHash, + ) -> Result { + let get_mnlist_diff_msg = NetworkMessage::GetMnListD(GetMnListDiff { + base_block_hash, + block_hash, + }); + self.send_dml_request_message(get_mnlist_diff_msg) + } + + /// Sends a `GetMnListDiff` request after completing the handshake. + pub fn get_qr_info( + &mut self, + known_block_hashes: Vec, + block_request_hash: BlockHash, + ) -> Result { + let get_mnlist_diff_msg = NetworkMessage::GetQRInfo(message_qrinfo::GetQRInfo { + base_block_hashes: known_block_hashes, + block_request_hash, + extra_share: true, + }); + self.send_qr_info_request_message(get_mnlist_diff_msg) + } +} diff --git a/src/components/core_zmq_listener.rs b/src/components/core_zmq_listener.rs index 86f35df32..e02a8f630 100644 --- a/src/components/core_zmq_listener.rs +++ b/src/components/core_zmq_listener.rs @@ -1,6 +1,6 @@ use crossbeam_channel::Sender; use dash_sdk::dpp::dashcore::consensus::Decodable; -use dash_sdk::dpp::dashcore::{Block, InstantLock, Network, Transaction}; +use dash_sdk::dpp::dashcore::{Block, ChainLock, InstantLock, Network, Transaction}; use dash_sdk::dpp::prelude::CoreBlockHeight; use std::error::Error; use std::io::Cursor; @@ -34,8 +34,7 @@ pub struct CoreZMQListener { pub enum ZMQMessage { ISLockedTransaction(Transaction, InstantLock), - ChainLockedBlock(#[allow(dead_code)] Block), - #[allow(dead_code)] // May be used for chain-locked transactions + ChainLockedBlock(Block, ChainLock), ChainLockedLockedTransaction(Transaction, CoreBlockHeight), } @@ -138,7 +137,7 @@ impl CoreZMQListener { let data_bytes = data_message.as_bytes(); match topic { - "rawchainlock" => { + "rawchainlocksig" => { // println!("Received raw chain locked block:"); // println!("Data (hex): {}", hex::encode(data_bytes)); @@ -148,20 +147,33 @@ impl CoreZMQListener { // Deserialize the LLMQChainLock match Block::consensus_decode(&mut cursor) { Ok(block) => { - // Send the ChainLock and Network back to the main thread - if let Err(e) = sender.send(( - ZMQMessage::ChainLockedBlock(block), - network, - )) { - eprintln!( - "Error sending data to main thread: {}", - e - ); + match ChainLock::consensus_decode(&mut cursor) { + Ok(chain_lock) => { + // Send the ChainLock and Network back to the main thread + if let Err(e) = sender.send(( + ZMQMessage::ChainLockedBlock( + block, chain_lock, + ), + network, + )) { + eprintln!( + "Error sending data to main thread: {}", + e + ); + } + } + Err(e) => { + eprintln!( + "Error deserializing InstantLock: {}", + e + ); + } } } Err(e) => { eprintln!( - "Error deserializing chain locked block: {}", + "Error deserializing chain locked block: bytes({}) error: {}", + hex::encode(data_bytes), e ); } @@ -328,23 +340,35 @@ impl CoreZMQListener { match topic.as_str() { "rawchainlock" => { - // Deserialize the Block + // Deserialize the Block followed by the ChainLock let mut cursor = Cursor::new(data_bytes); match Block::consensus_decode(&mut cursor) { Ok(block) => { - if let Some(ref tx) = tx_zmq_status { - // ZMQ refresh socket connected status - tx.send(ZMQConnectionEvent::Connected) - .expect("Failed to send connected event"); - } - if let Err(e) = sender.send(( - ZMQMessage::ChainLockedBlock(block), - network, - )) { - eprintln!( - "Error sending data to main thread: {}", - e - ); + match ChainLock::consensus_decode(&mut cursor) { + Ok(chain_lock) => { + if let Some(ref tx) = tx_zmq_status { + // ZMQ refresh socket connected status + tx.send(ZMQConnectionEvent::Connected) + .expect("Failed to send connected event"); + } + if let Err(e) = sender.send(( + ZMQMessage::ChainLockedBlock( + block, chain_lock, + ), + network, + )) { + eprintln!( + "Error sending data to main thread: {}", + e + ); + } + } + Err(e) => { + eprintln!( + "Error deserializing ChainLock: {}", + e + ); + } } } Err(e) => { diff --git a/src/components/mod.rs b/src/components/mod.rs index 6339bfa30..63b1335f0 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1 +1,2 @@ +pub mod core_p2p_handler; pub mod core_zmq_listener; diff --git a/src/config.rs b/src/config.rs index 1d0a7686a..bb3452bb9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,6 +40,8 @@ pub struct NetworkConfig { pub core_rpc_password: String, /// URL of the Insight API pub insight_api_url: String, + /// ZMQ endpoint for Core blockchain events (e.g., tcp://127.0.0.1:23708) + pub core_zmq_endpoint: Option, /// Devnet network name if one exists pub devnet_name: Option, /// Optional wallet private key to instantiate the wallet @@ -103,6 +105,15 @@ impl Config { ) .map_err(|e| ConfigError::LoadError(e.to_string()))?; + if let Some(core_zmq_endpoint) = &config.core_zmq_endpoint { + writeln!( + env_file, + "{}core_zmq_endpoint={}", + prefix, core_zmq_endpoint + ) + .map_err(|e| ConfigError::LoadError(e.to_string()))?; + } + if let Some(devnet_name) = &config.devnet_name { // Only write devnet name if it exists writeln!(env_file, "{}devnet_name={}", prefix, devnet_name) diff --git a/src/context.rs b/src/context.rs index 45e31ec7d..c828623d1 100644 --- a/src/context.rs +++ b/src/context.rs @@ -8,6 +8,7 @@ use crate::model::contested_name::ContestedName; use crate::model::password_info::PasswordInfo; use crate::model::qualified_contract::QualifiedContract; use crate::model::qualified_identity::{DPNSNameInfo, QualifiedIdentity}; +use crate::model::settings::Settings; use crate::model::wallet::{Wallet, WalletSeedHash}; use crate::sdk_wrapper::initialize_sdk; use crate::ui::RootScreenType; @@ -30,18 +31,23 @@ use dash_sdk::dpp::state_transition::StateTransitionSigningOptions; use dash_sdk::dpp::state_transition::batch_transition::methods::StateTransitionCreationOptions; use dash_sdk::dpp::system_data_contracts::{SystemDataContract, load_system_data_contract}; use dash_sdk::dpp::version::PlatformVersion; -use dash_sdk::dpp::version::v9::PLATFORM_V9; +use dash_sdk::dpp::version::v10::PLATFORM_V10; use dash_sdk::platform::{DataContract, Identifier}; use dash_sdk::query_types::IndexMap; use egui::Context; use rusqlite::Result; use std::collections::{BTreeMap, HashMap}; -use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Arc, Mutex, RwLock, RwLockWriteGuard}; const ANIMATION_REFRESH_TIME: std::time::Duration = std::time::Duration::from_millis(100); +/// A guard that ensures settings cache invalidation happens atomically +/// +/// This guard holds a write lock on the cached settings, preventing reads +/// until the database update is complete and the cache is properly invalidated. +type SettingsCacheGuard<'a> = RwLockWriteGuard<'a, Option>; + #[derive(Debug)] pub struct AppContext { pub(crate) network: Network, @@ -69,6 +75,9 @@ pub struct AppContext { /// This is used to control animations in the UI, such as loading spinners or transitions. /// Disable for automated tests. animate: AtomicBool, + /// Cached settings to avoid expensive database reads + /// Use RwLock to allow multiple readers but exclusive writers for cache invalidation + cached_settings: RwLock>, // subtasks started by the app context, used for graceful shutdown pub(crate) subtasks: Arc, } @@ -147,7 +156,9 @@ impl AppContext { .map(|w| (w.seed_hash(), Arc::new(RwLock::new(w)))) .collect(); - let animate = match config.developer_mode.unwrap_or(false) { + let developer_mode_enabled = config.developer_mode.unwrap_or(false); + + let animate = match developer_mode_enabled { true => { tracing::debug!("developer_mode is enabled, disabling animations"); AtomicBool::new(false) @@ -157,7 +168,7 @@ impl AppContext { let app_context = AppContext { network, - developer_mode: AtomicBool::new(config.developer_mode.unwrap_or(false)), + developer_mode: AtomicBool::new(developer_mode_enabled), devnet_name: None, db, sdk: sdk.into(), @@ -175,6 +186,7 @@ impl AppContext { transactions_waiting_for_finality: Mutex::new(BTreeMap::new()), zmq_connection_status: Mutex::new(ZMQConnectionEvent::Disconnected), animate, + cached_settings: RwLock::new(None), subtasks, }; @@ -471,25 +483,74 @@ impl AppContext { /// Updates the `start_root_screen` in the settings table pub fn update_settings(&self, root_screen_type: RootScreenType) -> Result<()> { + let _guard = self.invalidate_settings_cache(); + self.db .insert_or_update_settings(self.network, root_screen_type) } - /// Retrieves the current `RootScreenType` from the settings - #[allow(clippy::type_complexity)] - pub fn get_settings( + /// Updates the main password settings + pub fn update_main_password( + &self, + salt: &[u8], + nonce: &[u8], + password_check: &[u8], + ) -> Result<()> { + let _guard = self.invalidate_settings_cache(); + + self.db.update_main_password(salt, nonce, password_check) + } + + /// Updates the Dash Core execution settings + pub fn update_dash_core_execution_settings( &self, - ) -> Result< - Option<( - Network, - RootScreenType, - Option, - Option, - bool, - crate::ui::theme::ThemeMode, - )>, - > { - self.db.get_settings() + custom_dash_qt_path: Option, + overwrite_dash_conf: bool, + ) -> Result<()> { + let _guard = self.invalidate_settings_cache(); + + self.db + .update_dash_core_execution_settings(custom_dash_qt_path, overwrite_dash_conf) + } + + /// Invalidates the settings cache and returns a guard + /// + /// The cache is invalidated immediately and the guard prevents concurrent access + /// until the database operation is complete. This ensures atomicity and prevents + /// race conditions regardless of whether the database operation succeeds or fails. + pub fn invalidate_settings_cache(&'_ self) -> SettingsCacheGuard<'_> { + let mut guard = self.cached_settings.write().unwrap(); + *guard = None; + guard + } + + /// Retrieves the current settings + /// + /// ## Cached + /// + /// This function uses a cache to avoid expensive database operations. + /// The cache is invalidated when settings are updated. + /// + /// Use [`AppContext::invalidate_settings_cache`] to invalidate the cache. + pub fn get_settings(&self) -> Result> { + // First, try to read from cache + { + let cache = self.cached_settings.read().unwrap(); + if let Some(ref settings) = *cache { + return Ok(Some(settings.clone())); + } + } + + // Cache miss, read from database + let settings = self.db.get_settings()?.map(Settings::from); + + // Update cache with the fresh data + { + let mut cache = self.cached_settings.write().unwrap(); + *cache = settings.clone(); + } + + Ok(settings) } /// Retrieves all contracts from the database plus the system contracts from app context. @@ -765,6 +826,35 @@ impl AppContext { self.db.remove_token(token_id, self) } + pub fn remove_wallet(&self, seed_hash: &WalletSeedHash) -> Result<(), String> { + { + let wallets = self + .wallets + .read() + .map_err(|_| "Failed to access wallets".to_string())?; + if !wallets.contains_key(seed_hash) { + return Err("Wallet not found".to_string()); + } + } + + self.db + .remove_wallet(seed_hash, &self.network) + .map_err(|e| e.to_string())?; + + let mut wallets = self + .wallets + .write() + .map_err(|_| "Failed to update wallets".to_string())?; + + wallets.remove(seed_hash); + let has_wallet = !wallets.is_empty(); + drop(wallets); + + self.has_wallet.store(has_wallet, Ordering::Relaxed); + + Ok(()) + } + #[allow(dead_code)] // May be used for storing token balances pub fn insert_token_identity_balance( &self, @@ -795,10 +885,10 @@ impl AppContext { /// For certain releases like developer previews, we may want to only increment the platform version for non-mainnet. pub(crate) const fn default_platform_version(network: &Network) -> &'static PlatformVersion { match network { - Network::Dash => &PLATFORM_V9, - Network::Testnet => &PLATFORM_V9, - Network::Devnet => &PLATFORM_V9, - Network::Regtest => &PLATFORM_V9, + Network::Dash => &PLATFORM_V10, + Network::Testnet => &PLATFORM_V10, + Network::Devnet => &PLATFORM_V10, + Network::Regtest => &PLATFORM_V10, _ => panic!("unsupported network"), } } diff --git a/src/database/contracts.rs b/src/database/contracts.rs index 72e2dd765..9144f5915 100644 --- a/src/database/contracts.rs +++ b/src/database/contracts.rs @@ -56,29 +56,28 @@ impl Database { InsertTokensToo::SomeTokensShouldBeAdded(positions) => positions, }; for token_contract_position in positions { - if let Some(token_id) = data_contract.token_id(token_contract_position) { - if let Ok(token_configuration) = + if let Some(token_id) = data_contract.token_id(token_contract_position) + && let Ok(token_configuration) = data_contract.expected_token_configuration(token_contract_position) - { - let config = config::standard(); - let Some(serialized_token_configuration) = - bincode::encode_to_vec(token_configuration, config).ok() - else { - // We should always be able to serialize - return Ok(()); - }; - let token_name = token_configuration - .conventions() - .singular_form_by_language_code_or_default("en"); - self.insert_token( - &token_id, - token_name, - serialized_token_configuration.as_slice(), - &data_contract.id(), - token_contract_position, - app_context, - )?; - } + { + let config = config::standard(); + let Some(serialized_token_configuration) = + bincode::encode_to_vec(token_configuration, config).ok() + else { + // We should always be able to serialize + return Ok(()); + }; + let token_name = token_configuration + .conventions() + .singular_form_by_language_code_or_default("en"); + self.insert_token( + &token_id, + token_name, + serialized_token_configuration.as_slice(), + &data_contract.id(), + token_contract_position, + app_context, + )?; } } } diff --git a/src/database/identities.rs b/src/database/identities.rs index b0635d067..ac5c86f2c 100644 --- a/src/database/identities.rs +++ b/src/database/identities.rs @@ -177,6 +177,7 @@ impl Database { identity.wallet_index = wallet_index; identity.status = IdentityStatus::from_u8(status); + identity.network = app_context.network; // Associate wallets identity.associated_wallets = wallets.clone(); //todo: use less wallets @@ -232,6 +233,7 @@ impl Database { let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); identity.alias = alias; identity.wallet_index = wallet_index; + identity.network = app_context.network; // Associate wallets identity.associated_wallets = wallets.clone(); //todo: use less wallets @@ -287,6 +289,7 @@ impl Database { let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); identity.alias = alias; identity.wallet_index = wallet_index; + identity.network = app_context.network; // Associate wallets identity.associated_wallets = wallets.clone(); //todo: use less wallets @@ -326,7 +329,8 @@ impl Database { )?; let identity_iter = stmt.query_map(params![network], |row| { let data: Vec = row.get(0)?; - let identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); + let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); + identity.network = app_context.network; Ok(identity) })?; @@ -353,7 +357,8 @@ impl Database { stmt.query_map(params![network], |row| { let data: Vec = row.get(0)?; let wallet_id: Option = row.get(1)?; - let identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); + let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&data); + identity.network = app_context.network; Ok((identity, wallet_id)) })? diff --git a/src/database/settings.rs b/src/database/settings.rs index 080712ccc..eaa47bad2 100644 --- a/src/database/settings.rs +++ b/src/database/settings.rs @@ -9,6 +9,8 @@ use std::{path::PathBuf, str::FromStr}; impl Database { /// Inserts or updates the settings in the database. This method ensures that only one row exists. + /// + /// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior. pub fn insert_or_update_settings( &self, network: Network, @@ -27,6 +29,9 @@ impl Database { Ok(()) } + /// Updates the main password information in the settings table. + /// + /// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior. pub fn update_main_password( &self, salt: &[u8], @@ -45,7 +50,9 @@ impl Database { Ok(()) } - + /// Updates the Dash Core execution settings in the settings table. + /// + /// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior. pub fn update_dash_core_execution_settings( &self, custom_dash_qt_path: Option, @@ -112,7 +119,9 @@ impl Database { Ok(()) } - + /// Updates the theme preference in the settings table. + /// + /// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior. pub fn update_theme_preference(&self, theme_preference: ThemeMode) -> Result<()> { let theme_str = match theme_preference { ThemeMode::Light => "Light", @@ -144,6 +153,8 @@ impl Database { } /// Retrieves the settings from the database. + /// + /// Don't call this method directly, use `AppContext` methods instead to ensure proper caching behavior. #[allow(clippy::type_complexity)] pub fn get_settings( &self, diff --git a/src/database/wallet.rs b/src/database/wallet.rs index c27ae0d06..1812de9e7 100644 --- a/src/database/wallet.rs +++ b/src/database/wallet.rs @@ -8,7 +8,6 @@ use dash_sdk::dashcore_rpc::dashcore::Address; use dash_sdk::dashcore_rpc::dashcore::transaction::special_transaction::TransactionPayload; use dash_sdk::dpp::balances::credits::Duffs; use dash_sdk::dpp::dashcore::address::{NetworkChecked, NetworkUnchecked}; -use dash_sdk::dpp::dashcore::bip32::{DerivationPath, ExtendedPubKey}; use dash_sdk::dpp::dashcore::consensus::deserialize; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::{ @@ -17,6 +16,7 @@ use dash_sdk::dpp::dashcore::{ use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::state_transition::asset_lock_proof::InstantAssetLockProof; use dash_sdk::dpp::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; +use dash_sdk::dpp::key_wallet::bip32::{DerivationPath, ExtendedPubKey}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::dpp::prelude::{AssetLockProof, CoreBlockHeight}; use rusqlite::params; @@ -68,6 +68,45 @@ impl Database { Ok(()) } + /// Remove a wallet and all associated records from the database. + /// + /// This clears dependent records (addresses, utxos, asset locks, identity links) + /// to keep the database consistent before deleting the wallet itself. + pub fn remove_wallet(&self, seed_hash: &[u8; 32], network: &Network) -> rusqlite::Result<()> { + let network_str = network.to_string(); + let mut conn = self.conn.lock().unwrap(); + let tx = conn.transaction()?; + + let mut address_stmt = + tx.prepare("SELECT address FROM wallet_addresses WHERE seed_hash = ?")?; + let address_rows = + address_stmt.query_map(params![seed_hash], |row| row.get::<_, String>(0))?; + let mut addresses = Vec::new(); + for address in address_rows { + addresses.push(address?); + } + drop(address_stmt); + + for address in addresses { + tx.execute( + "DELETE FROM utxos WHERE address = ? AND network = ?", + params![address, &network_str], + )?; + } + + tx.execute( + "UPDATE identity SET wallet = NULL, wallet_index = NULL WHERE wallet = ? AND network = ?", + params![seed_hash, &network_str], + )?; + + tx.execute( + "DELETE FROM wallet WHERE seed_hash = ? AND network = ?", + params![seed_hash, &network_str], + )?; + + tx.commit() + } + /// Update only the alias and is_main fields of a wallet #[allow(dead_code)] // May be used for batch wallet metadata updates pub fn update_wallet_alias_and_main( @@ -467,6 +506,7 @@ impl Database { if let Some(wallet) = wallets_map.get_mut(&wallet_seed_hash_array) { let mut identity: QualifiedIdentity = QualifiedIdentity::from_bytes(&identity_data); identity.wallet_index = Some(wallet_index); + identity.network = *network; tracing::trace!( wallet_seed = ?wallet_seed_hash_array, diff --git a/src/model/amount.rs b/src/model/amount.rs new file mode 100644 index 000000000..8230f7c9b --- /dev/null +++ b/src/model/amount.rs @@ -0,0 +1,752 @@ +use bincode::{Decode, Encode}; +use dash_sdk::dpp::balances::credits::{CREDITS_PER_DUFF, Duffs, TokenAmount}; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Display}; + +/// How many decimal places are used for DASH amounts. +/// +/// This value is used to convert between DASH and credits. 1 DASH = 10.pow(DASH_DECIMAL_PLACES) +/// +/// 1 dash == 10e11 credits +pub const DASH_DECIMAL_PLACES: u8 = 11; + +/// Represents an amount of a token or cryptocurrency. +/// +/// As we cannot use floats to represent token amounts due to precision issues, we represent amounts as integers (u64) +/// with a specified number of decimal places. `Amount` is a generic type to handle these types of values. +/// +/// Internally, the value is stored as an integer (u64) representing the smallest unit of the +/// token (e.g., [Credits] for DASH), and the number of decimal places that is used to format it correctly. +#[derive(Serialize, Deserialize, Encode, Decode, Clone, PartialEq, Eq, Default)] +pub struct Amount { + /// Number of smallest units available for this token. + /// For example, for token value of `12.3450` with 4 decimal places, the stored value is `123450`. + value: u64, + /// Number of decimal places used for this token. + /// For example, for token value of `12.3450` that allows 4 decimal places, decimal_places is `4`. + decimal_places: u8, + unit_name: Option, +} + +impl PartialOrd for Amount { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.value.cmp(&other.value)) + } +} + +impl PartialEq for Amount { + fn eq(&self, other: &TokenAmount) -> bool { + self.value == *other + } +} + +impl PartialEq for &Amount { + fn eq(&self, other: &TokenAmount) -> bool { + self.value == *other + } +} + +impl Display for Amount { + /// Formats the TokenValue as a user-friendly string with optional unit name. + /// + /// See [`Amount::to_string_opts()`] for more formatting options. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let amount_str = self.to_string_opts(true, true); + write!(f, "{}", amount_str) + } +} + +impl Debug for Amount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Amount") + .field("value", &self.value) + .field("decimal_places", &self.decimal_places) + .field("unit_name", &self.unit_name) + .field("formatted", &self.to_string_without_unit()) + .finish() + } +} + +impl Amount { + /// Creates a new Amount. + /// + /// To set an unit name, use [Amount::with_unit_name]. + pub const fn new(value: TokenAmount, decimal_places: u8) -> Self { + Self { + value, + decimal_places, + unit_name: None, + } + } + + /// Creates a new Amount configured for a specific token. + /// + /// This extracts the decimal places and token alias from the token configuration + /// and creates an Amount with the specified value. + pub fn from_token( + token_info: &crate::ui::tokens::tokens_screen::IdentityTokenInfo, + value: TokenAmount, + ) -> Self { + let decimal_places = token_info.token_config.conventions().decimals(); + Self::new(value, decimal_places).with_unit_name(&token_info.token_alias) + } + + /// Creates a new Amount based on a floating-point value. + /// + /// Note that this is imprecise due to floating-point representation. Prefer using [Amount::new]. + pub fn try_from_f64(value: f64, decimal_places: u8) -> Result { + let value = checked_round(value * 10f64.powi(decimal_places as i32)) + .map_err(|e| format!("Invalid amount: {}", e))?; + Ok(Self::new(value, decimal_places)) + } + + /// Creates a new Amount from a string input with specified decimal places. + /// If the input string contains a unit suffix (e.g., "123.45 USD"), the unit name will be preserved. + pub fn parse(input: &str, decimal_places: u8) -> Result { + let (value, unit_name) = Self::parse_amount_string_with_unit(input, decimal_places)?; + match unit_name { + Some(unit) => Ok(Self::new(value, decimal_places).with_unit_name(&unit)), + None => Ok(Self::new(value, decimal_places)), + } + } + + /// Parses a string amount into the internal u64 representation. + /// Returns a tuple of (value, optional_unit_name). + /// Automatically extracts any unit suffix from the input string. + fn parse_amount_string_with_unit( + input: &str, + decimal_places: u8, + ) -> Result<(u64, Option), String> { + let input = input.trim(); + if input.is_empty() { + return Err("Invalid amount: cannot be empty".to_string()); + } + + // Split by whitespace to separate numeric part from potential unit + let parts: Vec<&str> = input.split_whitespace().collect(); + let numeric_part = parts.first().unwrap_or(&input); + let unit_name = if parts.len() > 1 { + Some(parts[1..].join(" ")) // Join remaining parts as unit name (handles multi-word units) + } else { + None + }; + + let value = Self::parse_numeric_part(numeric_part, decimal_places)?; + Ok((value, unit_name)) + } + + /// Parses the numeric part of an amount string. + fn parse_numeric_part(numeric_part: &str, decimal_places: u8) -> Result { + if decimal_places == 0 { + return numeric_part + .parse::() + .map_err(|e| format!("Invalid amount: {}", e)); + } + + let parts: Vec<&str> = numeric_part.split('.').collect(); + match parts.len() { + 1 => { + // No decimal point, parse as whole number + let whole = parts[0] + .parse::() + .map_err(|_| "Invalid amount: must be a number".to_string())?; + let multiplier = 10u64.pow(decimal_places as u32); + whole + .checked_mul(multiplier) + .ok_or_else(|| "Amount too large".to_string()) + } + 2 => { + // Has decimal point + let whole = if parts[0].is_empty() { + 0 + } else { + parts[0] + .parse::() + .map_err(|_| "Invalid amount: whole part must be a number".to_string())? + }; + + let fraction_str = parts[1]; + if fraction_str.len() > decimal_places as usize { + return Err(format!( + "Too many decimal places. Maximum allowed: {}", + decimal_places + )); + } + + // Pad with zeros if needed + let padded_fraction = + format!("{:0() + .map_err(|_| "Invalid amount: decimal part must be a number".to_string())?; + + let multiplier = 10u64.pow(decimal_places as u32); + let whole_part = whole + .checked_mul(multiplier) + .ok_or_else(|| "Amount too large".to_string())?; + + whole_part + .checked_add(fraction) + .ok_or_else(|| "Amount too large".to_string()) + } + _ => Err("Invalid amount: too many decimal points".to_string()), + } + } + + /// Converts the Amount to a f64 representation with the specified decimal places. + /// + /// Note this is a non-precise conversion, as f64 cannot represent all decimal values exactly. + pub fn to_f64(&self) -> f64 { + (self.value as f64) / 10u64.pow(self.decimal_places as u32) as f64 + } + + /// Returns the number of decimal places. + pub fn decimal_places(&self) -> u8 { + self.decimal_places + } + + /// Returns the value as the smallest unit (without decimal conversion). + pub fn value(&self) -> u64 { + self.value + } + + /// Returns the unit name if set. + pub fn unit_name(&self) -> Option<&str> { + self.unit_name.as_deref() + } + + /// Sets the unit name. + pub fn with_unit_name(mut self, unit_name: &str) -> Self { + if unit_name.is_empty() { + self.unit_name = None; + } else { + self.unit_name = Some(unit_name.to_string()); + } + + self + } + + /// Clears the unit name. + pub fn without_unit_name(mut self) -> Self { + self.unit_name = None; + self + } + + /// Returns the numeric string representation without the unit name. + /// Trailing zeroes are trimmed by default. + /// This is useful for text input fields where only the number should be shown. + /// + /// ## See also + /// + /// [`Amount::to_string_opts()`] for more formatting options. + pub fn to_string_without_unit(&self) -> String { + self.to_string_opts(false, true) + } + + /// Formats the Amount as a string with options for unit display and trailing zeroes. + pub fn to_string_opts(&self, show_unit: bool, trim_trailing_zeroes: bool) -> String { + let mut result = String::new(); + + let divisor = 10u64.pow(self.decimal_places as u32); + let whole = self.value / divisor; + let fraction = self.value % divisor; + + // "123" + result.push_str(&whole.to_string()); + + if self.decimal_places != 0 { + // "123.0000" + result.push_str(&format!( + ".{:0width$}", + fraction, + width = self.decimal_places as usize + )); + + if trim_trailing_zeroes { + // Remove trailing zeros + // "123." + result = result.trim_end_matches('0').to_string(); + } + // "123" + result = result.trim_end_matches('.').to_string(); + }; + + if show_unit + && let Some(unit_name) = self.unit_name.as_ref() + && !unit_name.is_empty() + { + result.push(' '); + result.push_str(unit_name); + } + + result + } + + /// Creates a new Amount with the specified value in TokenAmount. + pub fn with_value(mut self, value: TokenAmount) -> Self { + self.value = value; + self + } + + /// Checks if the amount is for the same token as the other amount. + /// + /// This is determined by comparing the unit names and decimal places. + pub fn is_same_token(&self, other: &Self) -> bool { + self.unit_name == other.unit_name && self.decimal_places == other.decimal_places + } +} + +/// Dash-specific amount handling +impl Amount { + /// Create a new [Amount] representing some value in DASH. + /// + /// Create [Amount] representation of some value in DASH cryptocurrency (eg. `1.5`). + /// + /// Note: Due to use of float, this may not be precise. Use [Amount::new()] for exact values. + pub fn new_dash(dash_value: f64) -> Self { + const MULTIPLIER: f64 = 10u64.pow(DASH_DECIMAL_PLACES as u32) as f64; + // internally we store DASH as [Credits] in the Amount.value field + let credits = dash_value * MULTIPLIER; + Self::new( + checked_round(credits).expect("DASH value overflow"), + DASH_DECIMAL_PLACES, + ) + .with_unit_name("DASH") + } + + /// Return Amount representing Dash currency equal to the given duffs. + /// + /// This is a special case where we get Duffs (eg. from Core) and want to convert it to an Amount representing DASH. + pub fn dash_from_duffs(duffs: Duffs) -> Self { + let credits = duffs * CREDITS_PER_DUFF; + Self::new(credits, DASH_DECIMAL_PLACES).with_unit_name("DASH") + } + + /// Returns the DASH amount as duffs, rounded down to the nearest integer. + /// + /// ## Returns + /// + /// Returns error if the token is not DASH, eg. decimals != DASH_DECIMAL_PLACES or token name is neither `DASH` nor empty. + pub fn dash_to_duffs(&self) -> Result { + if self.unit_name.as_ref().is_some_and(|name| name != "DASH") { + return Err("Amount is not in DASH".into()); + } + if self.decimal_places != DASH_DECIMAL_PLACES { + return Err("Amount is not in DASH, decimal places mismatch".into()); + } + + self.value + .checked_div(CREDITS_PER_DUFF) + .ok_or("Division by zero in DASH to duffs conversion".to_string()) + } +} + +impl AsRef for Amount { + /// Returns a reference to the Amount. + fn as_ref(&self) -> &Self { + self + } +} + +/// Conversion implementations for token types +impl From<&crate::ui::tokens::tokens_screen::IdentityTokenBalance> for Amount { + /// Converts an IdentityTokenBalance to an Amount. + /// + /// The decimal places are automatically determined from the token configuration, + /// and the token alias is used as the unit name. + fn from(token_balance: &crate::ui::tokens::tokens_screen::IdentityTokenBalance) -> Self { + let decimal_places = token_balance.token_config.conventions().decimals(); + Self::new(token_balance.balance, decimal_places).with_unit_name(&token_balance.token_alias) + } +} + +impl From for Amount { + /// Converts an owned IdentityTokenBalance to an Amount. + fn from(token_balance: crate::ui::tokens::tokens_screen::IdentityTokenBalance) -> Self { + Self::from(&token_balance) + } +} + +impl From<&crate::ui::tokens::tokens_screen::IdentityTokenBalanceWithActions> for Amount { + /// Converts an IdentityTokenBalanceWithActions to an Amount. + /// + /// The decimal places are automatically determined from the token configuration, + /// and the token alias is used as the unit name. + fn from( + token_balance: &crate::ui::tokens::tokens_screen::IdentityTokenBalanceWithActions, + ) -> Self { + let decimal_places = token_balance.token_config.conventions().decimals(); + Self::new(token_balance.balance, decimal_places).with_unit_name(&token_balance.token_alias) + } +} + +impl From for Amount { + /// Converts an owned IdentityTokenBalanceWithActions to an Amount. + fn from( + token_balance: crate::ui::tokens::tokens_screen::IdentityTokenBalanceWithActions, + ) -> Self { + Self::from(&token_balance) + } +} + +/// Helper function to convert f64 to u64, with checks for overflow. +/// It rounds the value to the nearest u64, ensuring it is within bounds. +fn checked_round(value: f64) -> Result { + let rounded = value.round(); + if rounded < u64::MIN as f64 || rounded > u64::MAX as f64 { + return Err("Overflow: value outside of bounds".to_string()); + } + + Ok(rounded as u64) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_token_amount_formatting() { + // Test 0 decimal places + assert_eq!(Amount::new(100, 0).to_string(), "100"); + + // Test 2 decimal places + assert_eq!(Amount::new(12345, 2).to_string(), "123.45"); + assert_eq!(Amount::new(12300, 2).to_string(), "123"); + assert_eq!(Amount::new(12340, 2).to_string(), "123.4"); + + // Test 8 decimal places + assert_eq!(Amount::new(100_000_000, 8).to_string(), "1"); + assert_eq!(Amount::new(150_000_000, 8).to_string(), "1.5"); + assert_eq!(Amount::new(123_456_789, 8).to_string(), "1.23456789"); + } + + #[test] + fn test_token_amount_parsing() { + // Test 0 decimal places + assert_eq!(Amount::parse("100", 0).unwrap(), 100); + + // Test 2 decimal places + assert_eq!(Amount::parse("123.45", 2).unwrap(), 12345); + assert_eq!(Amount::parse("123", 2).unwrap(), 12300); + assert_eq!(Amount::parse("123.4", 2).unwrap(), 12340); + + // Test 8 decimal places + assert_eq!(Amount::parse("1", 8).unwrap(), 100000000); + assert_eq!(Amount::parse("1.5", 8).unwrap(), 150000000); + assert_eq!(Amount::parse("1.23456789", 8).unwrap(), 123456789); + + assert_eq!(Amount::parse("1.5 DASH", 8).unwrap(), 150000000); + + // Test parsing amounts with unit suffixes + assert_eq!(Amount::parse("123.45 USD", 2).unwrap(), 12345); + assert_eq!(Amount::parse("1.0 BTC", 8).unwrap(), 100000000); + assert_eq!(Amount::parse("50 TOKEN", 0).unwrap(), 50); + } + + #[test] + fn test_dash_amounts() { + // Test Dash parsing + let dash_amount = Amount::parse("1.5", DASH_DECIMAL_PLACES).unwrap(); + assert_eq!(dash_amount.value(), 150_000_000_000); + assert_eq!(dash_amount.decimal_places(), DASH_DECIMAL_PLACES); + assert_eq!(dash_amount.unit_name(), None); // No unit name when not specified in input + + // Test Dash parsing with unit suffix + let dash_amount_with_unit = Amount::parse("1.5 DASH", DASH_DECIMAL_PLACES).unwrap(); + assert_eq!(dash_amount_with_unit.value(), 150_000_000_000); + assert_eq!(dash_amount_with_unit.decimal_places(), DASH_DECIMAL_PLACES); + assert_eq!(dash_amount_with_unit.unit_name(), Some("DASH")); + } + + #[test] + fn test_duffs_method() { + // Test creating DASH amounts from duffs + // 1 DASH = 100,000,000 duffs = 10^8 duffs + // 1 duff = 1000 credits (CREDITS_PER_DUFF) + // So 1 DASH = 10^8 * 10^3 = 10^11 credits + + let zero_duffs = Amount::dash_from_duffs(0); + assert_eq!(zero_duffs.value(), 0); + assert_eq!(zero_duffs.unit_name(), Some("DASH")); + assert_eq!(format!("{}", zero_duffs), "0 DASH"); + + let one_duff = Amount::dash_from_duffs(1); + assert_eq!(one_duff.value(), 1000); // 1 duff = 1000 credits + assert_eq!(one_duff.unit_name(), Some("DASH")); + assert_eq!(format!("{}", one_duff), "0.00000001 DASH"); + + let hundred_million_duffs = Amount::dash_from_duffs(100_000_000); // 1 DASH + assert_eq!(hundred_million_duffs.value(), 100_000_000_000); + assert_eq!(format!("{}", hundred_million_duffs), "1 DASH"); + + let one_and_half_dash_in_duffs = Amount::dash_from_duffs(150_000_000); // 1.5 DASH + assert_eq!(one_and_half_dash_in_duffs.value(), 150_000_000_000); + assert_eq!(format!("{}", one_and_half_dash_in_duffs), "1.5 DASH"); + } + + #[test] + fn test_to_duffs_method() { + // Test converting DASH amounts back to duffs + let one_dash = Amount::new_dash(1.0); + assert_eq!(one_dash.dash_to_duffs().unwrap(), 100_000_000); // 1 DASH = 10^8 duffs + + let half_dash = Amount::new_dash(0.5); + assert_eq!(half_dash.dash_to_duffs().unwrap(), 50_000_000); // 0.5 DASH = 5*10^7 duffs + + let one_and_half_dash = Amount::new_dash(1.5); + assert_eq!(one_and_half_dash.dash_to_duffs().unwrap(), 150_000_000); // 1.5 DASH = 1.5*10^8 duffs + + // Test with very small amounts + let one_credit = Amount::new(1, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + assert_eq!(one_credit.dash_to_duffs().unwrap(), 0); // 1 credit = 0 duffs (rounded down) + + let thousand_credits = Amount::new(1000, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + assert_eq!(thousand_credits.dash_to_duffs().unwrap(), 1); // 1000 credits = 1 duff + + // Test with amount without unit name (should work) + let dash_no_unit = Amount::new(100_000_000_000, DASH_DECIMAL_PLACES); + assert_eq!(dash_no_unit.dash_to_duffs().unwrap(), 100_000_000); + } + + #[test] + #[should_panic(expected = "Amount is not in DASH")] + fn test_to_duffs_panics_with_wrong_unit() { + let btc_amount = Amount::new(100_000_000, 8).with_unit_name("BTC"); + btc_amount.dash_to_duffs().unwrap(); // Should panic + } + + #[test] + #[should_panic(expected = "Amount is not in DASH, decimal places mismatch")] + fn test_to_duffs_panics_with_wrong_decimals() { + let wrong_decimals = Amount::new(100_000_000, 8).with_unit_name("DASH"); + wrong_decimals.dash_to_duffs().unwrap(); // Should panic + } + + #[test] + fn test_dash_duffs_roundtrip() { + // Test that duffs -> DASH -> duffs preserves the value + let original_duffs = 123_456_789u64; + let dash_amount = Amount::dash_from_duffs(original_duffs); + let converted_back = dash_amount.dash_to_duffs().unwrap(); + assert_eq!(original_duffs, converted_back); + + // Test edge cases + let zero_duffs = 0u64; + let zero_dash = Amount::dash_from_duffs(zero_duffs); + assert_eq!(zero_duffs, zero_dash.dash_to_duffs().unwrap()); + + let max_reasonable_duffs = 2_100_000_000_000_000u64; // 21M DASH * 10^8 + let max_dash = Amount::dash_from_duffs(max_reasonable_duffs); + assert_eq!(max_reasonable_duffs, max_dash.dash_to_duffs().unwrap()); + assert_eq!(max_reasonable_duffs * CREDITS_PER_DUFF, max_dash.value()); + assert_eq!(21_000_000.0, max_dash.to_f64()); + } + + #[test] + fn test_dash_precision() { + // Test that the dash() method handles precision correctly + // Note: Due to f64 limitations, very precise decimals might have rounding issues + + // Test values that should be exact in f64 + let exact_values = [0.0, 0.5, 1.0, 1.5, 2.0, 10.0, 100.0]; + for &value in &exact_values { + let dash_amount = Amount::new_dash(value); + let expected_credits = (value * 100_000_000_000.0).round() as u64; + assert_eq!(dash_amount.value(), expected_credits); + } + + // Test a value with 11 decimal places (max precision for DASH) + let precise_dash = Amount::new_dash(1.23456789012); // This might lose precision due to f64 + // We mainly test that it doesn't panic and creates a valid amount + assert!(precise_dash.value() > 0); + assert_eq!(precise_dash.unit_name(), Some("DASH")); + } + + #[test] + fn test_amount_display() { + let amount = Amount::new(12_345, 2); + assert_eq!(format!("{}", amount), "123.45"); + + let dash_amount = Amount::new_dash(1.5); + assert_eq!(format!("{}", dash_amount), "1.5 DASH"); + + // Test amount with custom unit name + let amount_with_unit = Amount::new(54321, 2).with_unit_name("USD"); + assert_eq!(format!("{}", amount_with_unit), "543.21 USD"); + } + + #[test] + fn test_unit_name_functionality() { + // Test creating amount with unit name + let amount = Amount::new(12345, 2).with_unit_name("USD"); + assert_eq!(amount.unit_name(), Some("USD")); + assert_eq!(amount.value(), 12345); + assert_eq!(amount.decimal_places(), 2); + assert_eq!(format!("{}", amount), "123.45 USD"); + + // Test adding unit name to existing amount + let amount = Amount::new(54321, 8).with_unit_name("BTC"); + assert_eq!(amount.unit_name(), Some("BTC")); + + // Test removing unit name + let amount = amount.without_unit_name(); + assert_eq!(amount.unit_name(), None); + + // Test Dash amounts include unit name + let dash_amount = Amount::new_dash(1.0); + assert_eq!(dash_amount.unit_name(), Some("DASH")); + + // Test parsing with_unit_name + let parsed = Amount::parse("123.45", 2).unwrap().with_unit_name("TOKEN"); + assert_eq!(parsed.unit_name(), Some("TOKEN")); + assert_eq!(parsed.value(), 12345); + } + + #[test] + fn test_parsing_errors() { + // Empty input + assert!(Amount::parse("", 2).is_err()); + + // Too many decimal places + assert!(Amount::parse("1.123", 2).is_err()); + + // Invalid characters + assert!(Amount::parse("abc", 2).is_err()); + + // Multiple decimal points + assert!(Amount::parse("1.2.3", 2).is_err()); + } + + #[test] + fn test_simplified_parsing_with_units() { + // Test the simplified API pattern: parse_with_decimals now preserves unit names automatically + let token_amount = Amount::parse("123.45 TOKEN", 2).unwrap(); + assert_eq!(token_amount.value(), 12345); + assert_eq!(token_amount.unit_name(), Some("TOKEN")); + assert_eq!(format!("{}", token_amount), "123.45 TOKEN"); + + // Test parsing with unit suffix automatically preserves the unit + let btc_amount = Amount::parse("0.5 BTC", 8).unwrap(); + assert_eq!(btc_amount.value(), 50000000); + assert_eq!(btc_amount.unit_name(), Some("BTC")); + assert_eq!(format!("{}", btc_amount), "0.5 BTC"); + + // Test parsing without unit in string results in no unit name + let no_unit_amount = Amount::parse("1.5", 11).unwrap(); + assert_eq!(no_unit_amount.value(), 150_000_000_000); + assert_eq!(no_unit_amount.unit_name(), None); + assert_eq!(format!("{}", no_unit_amount), "1.5"); + + // Test adding unit name manually when not present in string + let dash_amount = Amount::parse("1.5", 11).unwrap().with_unit_name("DASH"); + assert_eq!(dash_amount.value(), 150_000_000_000); + assert_eq!(dash_amount.unit_name(), Some("DASH")); + assert_eq!(format!("{}", dash_amount), "1.5 DASH"); + + // Test multi-word unit names + let multi_word_unit = Amount::parse("100 US Dollar", 2).unwrap(); + assert_eq!(multi_word_unit.value(), 10000); + assert_eq!(multi_word_unit.unit_name(), Some("US Dollar")); + assert_eq!(format!("{}", multi_word_unit), "100 US Dollar"); + } + + #[test] + fn test_to_string_without_unit() { + // Test amount without unit + let amount = Amount::new(12345, 2); + assert_eq!(amount.to_string_without_unit(), "123.45"); + assert_eq!(format!("{}", amount), "123.45"); // Display should be the same + + // Test amount with unit + let amount_with_unit = Amount::new(12345, 2).with_unit_name("USD"); + assert_eq!(amount_with_unit.to_string_without_unit(), "123.45"); // Without unit + assert_eq!(format!("{}", amount_with_unit), "123.45 USD"); // Display includes unit + + // Test Dash amount + let dash_amount = Amount::new_dash(1.5); // 1.5 DASH + assert_eq!(dash_amount.to_string_without_unit(), "1.5"); + assert_eq!(format!("{}", dash_amount), "1.5 DASH"); + assert_eq!(dash_amount.dash_to_duffs().unwrap(), 150_000_000); // 1.5 DASH in duffs + + // Test zero amount + let zero_amount = Amount::new(0, 8); + assert_eq!(zero_amount.to_string_without_unit(), "0"); + } + + #[test] + fn test_to_string_opts() { + // Test basic formatting options with 2 decimal places + let amount = Amount::new(12345, 2).with_unit_name("USD"); + + // Test all combinations of show_unit and trim_trailing_zeroes + assert_eq!(amount.to_string_opts(true, true), "123.45 USD"); // show unit, trim zeros + assert_eq!(amount.to_string_opts(false, true), "123.45"); // no unit, trim zeros + assert_eq!(amount.to_string_opts(true, false), "123.45 USD"); // show unit, no trim (same as above since no trailing zeros) + assert_eq!(amount.to_string_opts(false, false), "123.45"); // no unit, no trim (same as above since no trailing zeros) + + // Test with trailing zeros + let amount_with_zeros = Amount::new(12300, 2).with_unit_name("USD"); + assert_eq!(amount_with_zeros.to_string_opts(true, true), "123 USD"); // show unit, trim zeros + assert_eq!(amount_with_zeros.to_string_opts(false, true), "123"); // no unit, trim zeros + assert_eq!(amount_with_zeros.to_string_opts(true, false), "123.00 USD"); // show unit, no trim + assert_eq!(amount_with_zeros.to_string_opts(false, false), "123.00"); // no unit, no trim + + // Test with partial trailing zeros + let amount_partial_zeros = Amount::new(12340, 2).with_unit_name("USD"); + assert_eq!(amount_partial_zeros.to_string_opts(true, true), "123.4 USD"); // show unit, trim zeros + assert_eq!(amount_partial_zeros.to_string_opts(false, true), "123.4"); // no unit, trim zeros + assert_eq!( + amount_partial_zeros.to_string_opts(true, false), + "123.40 USD" + ); // show unit, no trim + assert_eq!(amount_partial_zeros.to_string_opts(false, false), "123.40"); // no unit, no trim + + // Test with 0 decimal places + let whole_amount = Amount::new(123, 0).with_unit_name("WHOLE"); + assert_eq!(whole_amount.to_string_opts(true, true), "123 WHOLE"); + assert_eq!(whole_amount.to_string_opts(false, true), "123"); + assert_eq!(whole_amount.to_string_opts(true, false), "123 WHOLE"); + assert_eq!(whole_amount.to_string_opts(false, false), "123"); + + // Test with high decimal places + let high_precision = Amount::new(123456789, 8).with_unit_name("BTC"); + assert_eq!(high_precision.to_string_opts(true, true), "1.23456789 BTC"); // trim zeros + assert_eq!(high_precision.to_string_opts(false, true), "1.23456789"); // trim zeros + assert_eq!(high_precision.to_string_opts(true, false), "1.23456789 BTC"); // no trim (same as above since no trailing zeros) + assert_eq!(high_precision.to_string_opts(false, false), "1.23456789"); // no trim (same as above since no trailing zeros) + + // Test with high decimal places and trailing zeros + let high_precision_zeros = Amount::new(100000000, 8).with_unit_name("BTC"); + assert_eq!(high_precision_zeros.to_string_opts(true, true), "1 BTC"); // trim zeros + assert_eq!(high_precision_zeros.to_string_opts(false, true), "1"); // trim zeros + assert_eq!( + high_precision_zeros.to_string_opts(true, false), + "1.00000000 BTC" + ); // no trim + assert_eq!( + high_precision_zeros.to_string_opts(false, false), + "1.00000000" + ); // no trim + + // Test zero amount + let zero_amount = Amount::new(0, 4).with_unit_name("TOKEN"); + assert_eq!(zero_amount.to_string_opts(true, true), "0 TOKEN"); + assert_eq!(zero_amount.to_string_opts(false, true), "0"); + assert_eq!(zero_amount.to_string_opts(true, false), "0.0000 TOKEN"); + assert_eq!(zero_amount.to_string_opts(false, false), "0.0000"); + + // Test amount without unit name + let no_unit = Amount::new(12345, 3); + assert_eq!(no_unit.to_string_opts(true, true), "12.345"); // show_unit=true but no unit name + assert_eq!(no_unit.to_string_opts(false, true), "12.345"); // show_unit=false + assert_eq!(no_unit.to_string_opts(true, false), "12.345"); // show_unit=true but no unit name, no trim + assert_eq!(no_unit.to_string_opts(false, false), "12.345"); // show_unit=false, no trim + + // Test amount with empty unit name (should be treated as no unit) + let empty_unit = Amount::new(12345, 2).with_unit_name(""); + assert_eq!(empty_unit.to_string_opts(true, true), "123.45"); // empty unit name should not show + assert_eq!(empty_unit.to_string_opts(false, true), "123.45"); + } +} diff --git a/src/model/grovestark_prover.rs b/src/model/grovestark_prover.rs new file mode 100644 index 000000000..7ce95a826 --- /dev/null +++ b/src/model/grovestark_prover.rs @@ -0,0 +1,530 @@ +use dash_sdk::Sdk; +use dash_sdk::dpp::document::DocumentV0Getters; +use dash_sdk::dpp::identifier::Identifier; +use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dash_sdk::dpp::identity::{KeyID, KeyType}; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::platform::documents::document_query::DocumentQuery; +use dash_sdk::platform::{ + Document, DriveDocumentQuery, Fetch, FetchMany, IdentityKeysQuery, IdentityPublicKey, +}; +use ed25519_dalek::{Signer, SigningKey}; +use grovestark::{ + GroveSTARK, PublicInputs, STARKConfig, STARKProof, create_witness_from_platform_proofs, +}; +use serde::{Deserialize, Serialize}; +use std::time::Instant; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProofDataOutput { + pub proof: Vec, // Serialized STARK proof + pub public_inputs: PublicInputsData, + pub metadata: ProofMetadata, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PublicInputsData { + pub state_root: [u8; 32], + pub contract_id: [u8; 32], + pub message_hash: [u8; 32], + pub timestamp: u64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProofMetadata { + pub created_at: u64, + pub proof_size: usize, + pub generation_time_ms: u64, + pub security_level: u32, +} + +pub struct GroveSTARKProver { + prover: GroveSTARK, +} + +impl Default for GroveSTARKProver { + fn default() -> Self { + Self::new() + } +} + +impl GroveSTARKProver { + pub fn new() -> Self { + // Use GroveSTARK's default config + let config = STARKConfig::default(); + + Self { + prover: GroveSTARK::with_config(config), + } + } + + /// Generate a proof for document ownership + #[allow(clippy::too_many_arguments)] + pub async fn generate_proof( + &self, + sdk: &Sdk, + identity_id: &str, + contract_id: &str, + document_type: &str, + document_id: &str, + key_id: u32, + private_key: &[u8; 32], + public_key: &[u8; 32], + ) -> Result { + if cfg!(debug_assertions) { + return Err(GroveSTARKError::UnsupportedBuild( + "GroveSTARK proof generation requires a release build (cargo run --release)" + .to_string(), + )); + } + + let start_time = Instant::now(); + + tracing::info!("Starting ZK proof generation"); + tracing::info!("Identity ID: {}", identity_id); + tracing::info!("Contract ID: {}", contract_id); + tracing::info!("Document Type: {}", document_type); + tracing::info!("Document ID: {}", document_id); + + // Step 1: Parse identifiers + tracing::debug!("Parsing identifiers..."); + let identity_identifier = + Identifier::from_string(identity_id, Encoding::Base58).map_err(|e| { + tracing::error!("Failed to parse identity ID: {}", e); + GroveSTARKError::InvalidIdentityId(e.to_string()) + })?; + let contract_identifier = Identifier::from_string(contract_id, Encoding::Base58) + .map_err(|e| GroveSTARKError::InvalidContractId(e.to_string()))?; + + // Step 2: Fetch specific key with proof using new SDK API + tracing::info!("Fetching specific key {} with proof...", key_id); + + // Create a query for the specific key + let specific_key_ids: Vec = vec![key_id]; + let keys_query = IdentityKeysQuery::new(identity_identifier, specific_key_ids); + + // Fetch only the specified key with proof + let (specific_keys, _metadata, key_proof) = + IdentityPublicKey::fetch_many_with_metadata_and_proof(sdk, keys_query, None) + .await + .map_err(|e| { + tracing::error!("Failed to fetch key with proof: {}", e); + GroveSTARKError::Platform(e.to_string()) + })?; + + // Verify the key exists in the identity + let identity_key = specific_keys + .get(&key_id) + .and_then(|maybe_key| maybe_key.as_ref()) + .ok_or_else(|| { + tracing::error!("Key {} not found for identity", key_id); + GroveSTARKError::PrivateKeyNotAvailable + })?; + + // Verify it's an EdDSA key + if identity_key.key_type() != KeyType::EDDSA_25519_HASH160 { + return Err(GroveSTARKError::InvalidProof( + "Key is not EdDSA type required for ZK proofs".to_string(), + )); + } + + // Use the public key passed from the UI (derived from private key) + let public_key_bytes = *public_key; + + // 3. KEY PROOF (Raw bytes) + tracing::info!("=== 3. KEY PROOF (Raw bytes) ==="); + tracing::info!("Key proof size: {} bytes", key_proof.grovedb_proof.len()); + tracing::info!("Key proof hex: {}", hex::encode(&key_proof.grovedb_proof)); + + // Additional key details + tracing::info!("Key ID: {}", key_id); + tracing::info!("Key type: {:?}", identity_key.key_type()); + tracing::info!("Key purpose: {:?}", identity_key.purpose()); + tracing::info!( + "Identity key data (hash160): {} bytes - {}", + identity_key.data().len(), + hex::encode(identity_key.data().to_vec()) + ); + + // Step 3: Fetch contract and create DocumentQuery + tracing::info!("Fetching contract..."); + let contract = dash_sdk::platform::DataContract::fetch(sdk, contract_identifier) + .await + .map_err(|e| { + tracing::error!("Failed to fetch contract: {}", e); + GroveSTARKError::Platform(e.to_string()) + })? + .ok_or_else(|| { + tracing::error!("Contract not found for ID: {}", contract_id); + GroveSTARKError::InvalidContractId("Contract not found".to_string()) + })?; + + let document_id_identifier = Identifier::from_string( + document_id, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + ) + .map_err(|e| GroveSTARKError::Platform(e.to_string()))?; + + let query = DocumentQuery::new(contract, document_type) + .map_err(|e| GroveSTARKError::Platform(e.to_string()))? + .with_document_id(&document_id_identifier); + + tracing::info!("Fetching document with proof..."); + let (document_opt, _metadata, proof) = + Document::fetch_with_metadata_and_proof(sdk, query.clone(), None) + .await + .map_err(|e| { + tracing::error!("Failed to fetch document with proof: {}", e); + GroveSTARKError::Platform(e.to_string()) + })?; + + let document = document_opt.ok_or_else(|| { + tracing::error!("Document not found for ID: {}", document_id); + GroveSTARKError::DocumentNotFound + })?; + + // COMPREHENSIVE LOGGING FOR DEBUGGING + + // 1. REAL DOCUMENT (JSON format) + tracing::info!("=== 1. REAL DOCUMENT (JSON FORMAT) ==="); + if let Ok(json_value) = serde_json::to_value(&document) { + let json_pretty = serde_json::to_string_pretty(&json_value).unwrap_or_default(); + tracing::info!( + "Full JSON document as returned by Platform:\n{}", + json_pretty + ); + + // Also log specific fields we care about + if let Some(owner_id_value) = json_value.get("$ownerId") { + tracing::info!("$ownerId field in document: {}", owner_id_value); + } + if let Some(id_value) = json_value.get("$id") { + tracing::info!("$id field in document: {}", id_value); + } + if let Some(revision_value) = json_value.get("$revision") { + tracing::info!("$revision field in document: {}", revision_value); + } + } + + // For witness creation, we need proper serialization + let document_cbor = serde_json::to_vec(&document).map_err(|e| { + GroveSTARKError::SerializationError(format!("Failed to encode document: {}", e)) + })?; + + // 5. EXPECTED VALUES FOR VERIFICATION + let document_owner_id = document.owner_id(); + tracing::info!("=== 5. EXPECTED VALUES FOR VERIFICATION ==="); + tracing::info!( + "Document owner_id (base58): {}", + document_owner_id + .to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58) + ); + tracing::info!( + "Document owner_id (hex): {}", + hex::encode(document_owner_id.to_buffer()) + ); + tracing::info!( + "Document owner_id (raw bytes): {:?}", + document_owner_id.to_buffer() + ); + + tracing::info!( + "Identity_id we're proving for (base58): {}", + identity_identifier + .to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58) + ); + tracing::info!( + "Identity_id we're proving for (hex): {}", + hex::encode(identity_identifier.to_buffer()) + ); + tracing::info!( + "Identity_id we're proving for (raw bytes): {:?}", + identity_identifier.to_buffer() + ); + + // Ownership verification status + if document_owner_id == identity_identifier { + tracing::info!( + "✅ OWNER MATCH: Document owner matches proving identity - proof should succeed" + ); + } else { + tracing::warn!( + "⚠️ OWNER MISMATCH: Document owner does NOT match proving identity - proof should fail!" + ); + } + + // 2. DOCUMENT PROOF (Raw bytes) + tracing::info!("=== 2. DOCUMENT PROOF (Raw bytes) ==="); + tracing::info!("Document proof size: {} bytes", proof.grovedb_proof.len()); + tracing::info!("Document proof hex: {}", hex::encode(&proof.grovedb_proof)); + + // Step 4: Get current state root by verifying document proof + let drive_document_query: DriveDocumentQuery = (&query) + .try_into() + .map_err(|e: dash_sdk::error::Error| GroveSTARKError::Platform(e.to_string()))?; + let (state_root, _documents) = drive_document_query + .verify_proof(&proof.grovedb_proof, sdk.version()) + .map_err(|e| { + tracing::error!("Failed to verify document proof: {}", e); + GroveSTARKError::InvalidProof(e.to_string()) + })?; + + tracing::info!( + "Document proof root hash (hex): {}", + hex::encode(state_root) + ); + tracing::info!("Document proof root hash (raw bytes): {:?}", state_root); + + // Step 5: Create signing challenge + let challenge = create_challenge(&state_root, contract_id, document_id); + + // Step 6: Sign the challenge with Ed25519 (we don't use this signature in the new approach) + // The witness creation will handle the signing internally + + // Step 7: Log proof information + tracing::info!( + "Using separate proofs - key: {} bytes, document: {} bytes", + key_proof.grovedb_proof.len(), + proof.grovedb_proof.len() + ); + + // 6. OPTIONAL BUT HELPFUL + tracing::info!("=== 6. OPTIONAL BUT HELPFUL ==="); + tracing::info!("Contract ID (base58): {}", contract_id); + tracing::info!( + "Contract ID (hex): {}", + hex::encode(contract_identifier.to_buffer()) + ); + tracing::info!("Document Type: {}", document_type); + tracing::info!("Document ID (base58): {}", document_id); + tracing::info!( + "Document ID (hex): {}", + hex::encode(document_id_identifier.to_buffer()) + ); + tracing::info!("State root (hex): {}", hex::encode(state_root)); + tracing::info!("State root (raw bytes): {:?}", state_root); + + // Document CBOR details + tracing::info!("Document CBOR size: {} bytes", document_cbor.len()); + if document_cbor.len() <= 500 { + tracing::info!("Document CBOR (hex): {}", hex::encode(&document_cbor)); + } else { + tracing::info!( + "Document CBOR (first 500 bytes hex): {}", + hex::encode(&document_cbor[..500]) + ); + } + + // 4. EdDSA SIGNATURE COMPONENTS + tracing::info!("=== 4. EdDSA SIGNATURE COMPONENTS ==="); + + // Sign the challenge message + let signing_key = SigningKey::from_bytes(private_key); + let signature = signing_key.sign(&challenge); + let sig_bytes = signature.to_bytes(); + let mut signature_r = [0u8; 32]; + let mut signature_s = [0u8; 32]; + signature_r.copy_from_slice(&sig_bytes[0..32]); + signature_s.copy_from_slice(&sig_bytes[32..64]); + + tracing::info!("Signature R (hex): {}", hex::encode(signature_r)); + tracing::info!("Signature R (raw bytes): {:?}", signature_r); + tracing::info!("Signature S (hex): {}", hex::encode(signature_s)); + tracing::info!("Signature S (raw bytes): {:?}", signature_s); + tracing::info!("Public key (hex): {}", hex::encode(public_key_bytes)); + tracing::info!("Public key (raw bytes): {:?}", public_key_bytes); + tracing::info!("Message/Challenge (hex): {}", hex::encode(challenge)); + tracing::info!("Message/Challenge (raw bytes): {:?}", challenge); + + // Step 8: Use GroveSTARK's new platform proofs V2 API + tracing::info!("Creating witness with GroveSTARK platform proofs V2..."); + + let witness = create_witness_from_platform_proofs( + &proof.grovedb_proof, // Raw document proof from SDK + &key_proof.grovedb_proof, // Raw key proof from SDK + document_cbor.clone(), // Use the proper CBOR we created above + &public_key_bytes, // Public key bytes + &signature_r, // Signature R component + &signature_s, // Signature s component + &challenge, // Message to sign + ) + .map_err(|e| { + tracing::error!("GroveSTARK witness creation failed: {:?}", e); + GroveSTARKError::ProofGenerationFailed(format!( + "GroveSTARK witness creation failed: {:?}", + e + )) + })?; + + tracing::info!("Witness created successfully"); + + // Step 8: Prepare public inputs + let public_inputs = PublicInputs { + state_root, + contract_id: contract_identifier.to_buffer(), + message_hash: challenge, + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|e| GroveSTARKError::TimeError(e.to_string()))? + .as_secs(), + }; + + // Step 9: Generate the STARK proof + tracing::info!("Generating STARK proof (this normally takes around 10 seconds)..."); + eprintln!("Rayon thread pool size: {}", rayon::current_num_threads()); + let proof = self + .prover + .prove(witness, public_inputs.clone()) + .map_err(|e| { + tracing::error!("STARK proof generation failed: {}", e); + GroveSTARKError::ProofGenerationFailed(e.to_string()) + })?; + + tracing::info!("STARK proof generated successfully"); + + // Step 10: Serialize the proof + let serialized_proof = serde_json::to_vec(&proof) + .map_err(|e| GroveSTARKError::SerializationError(e.to_string()))?; + + let generation_time = start_time.elapsed(); + tracing::info!( + "Total proof generation time: {:.2}s", + generation_time.as_secs_f32() + ); + + Ok(ProofDataOutput { + proof: serialized_proof.clone(), + public_inputs: PublicInputsData { + state_root: public_inputs.state_root, + contract_id: public_inputs.contract_id, + message_hash: public_inputs.message_hash, + timestamp: public_inputs.timestamp, + }, + metadata: ProofMetadata { + created_at: public_inputs.timestamp, + proof_size: serialized_proof.len(), + generation_time_ms: generation_time.as_millis() as u64, + security_level: 128, // Default security level + }, + }) + } + + /// Verify a proof + pub fn verify_proof(&self, proof_data: &ProofDataOutput) -> Result { + if cfg!(debug_assertions) { + tracing::warn!("GroveSTARK proof verification attempted in debug build; aborting"); + return Err(GroveSTARKError::UnsupportedBuild( + "GroveSTARK proof verification requires a release build (cargo run --release)" + .to_string(), + )); + } + + // Step 1: Deserialize the proof + let stark_proof: STARKProof = serde_json::from_slice(&proof_data.proof) + .map_err(|e| GroveSTARKError::DeserializationError(e.to_string()))?; + + // Step 2: Reconstruct public inputs + let public_inputs = PublicInputs { + state_root: proof_data.public_inputs.state_root, + contract_id: proof_data.public_inputs.contract_id, + message_hash: proof_data.public_inputs.message_hash, + timestamp: proof_data.public_inputs.timestamp, + }; + + // Step 3: Verify the proof using GroveSTARK's verify method + self.prover + .verify(&stark_proof, &public_inputs) + .map_err(|e| GroveSTARKError::VerificationFailed(e.to_string())) + } +} + +impl ProofDataOutput { + /// Serialize the proof to JSON string + pub fn to_json_string(&self) -> Result { + serde_json::to_string(self).map_err(|e| GroveSTARKError::SerializationError(e.to_string())) + } + + /// Serialize the proof to base64-encoded JSON + pub fn to_base64(&self) -> Result { + use base64::{Engine as _, engine::general_purpose}; + let json_bytes = serde_json::to_vec(self) + .map_err(|e| GroveSTARKError::SerializationError(e.to_string()))?; + Ok(general_purpose::STANDARD.encode(json_bytes)) + } + + /// Deserialize from base64-encoded JSON + pub fn from_base64(base64_str: &str) -> Result { + use base64::{Engine as _, engine::general_purpose}; + let bytes = general_purpose::STANDARD.decode(base64_str).map_err(|e| { + GroveSTARKError::DeserializationError(format!("Base64 decode error: {}", e)) + })?; + serde_json::from_slice(&bytes) + .map_err(|e| GroveSTARKError::DeserializationError(e.to_string())) + } + + /// Deserialize from JSON string + pub fn from_json_string(json_str: &str) -> Result { + serde_json::from_str(json_str) + .map_err(|e| GroveSTARKError::DeserializationError(e.to_string())) + } +} + +/// Create a challenge message for signing +fn create_challenge(state_root: &[u8; 32], contract_id: &str, document_id: &str) -> [u8; 32] { + use sha2::{Digest, Sha256}; + + let mut hasher = Sha256::new(); + hasher.update(state_root); + hasher.update(contract_id.as_bytes()); + hasher.update(document_id.as_bytes()); + + let result = hasher.finalize(); + let mut hash = [0u8; 32]; + hash.copy_from_slice(&result); + hash +} + +#[derive(Debug, thiserror::Error)] +pub enum GroveSTARKError { + #[error("Platform error: {0}")] + Platform(String), + + #[error("Invalid identity ID: {0}")] + InvalidIdentityId(String), + + #[error("Invalid contract ID: {0}")] + InvalidContractId(String), + + #[error("Identity not found")] + IdentityNotFound, + + #[error("Document not found")] + DocumentNotFound, + + #[error("Private key not available")] + PrivateKeyNotAvailable, + + #[error("Proof generation failed: {0}")] + ProofGenerationFailed(String), + + #[error("Proof verification failed: {0}")] + VerificationFailed(String), + + #[error("Serialization error: {0}")] + SerializationError(String), + + #[error("Deserialization error: {0}")] + DeserializationError(String), + + #[error("Signing failed: {0}")] + SigningFailed(String), + + #[error("Invalid proof: {0}")] + InvalidProof(String), + + #[error("Time error: {0}")] + TimeError(String), + + #[error("{0}")] + UnsupportedBuild(String), +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 105cce062..ef4ce0794 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,6 +1,9 @@ +pub mod amount; pub mod contested_name; +pub mod grovestark_prover; pub mod password_info; pub mod proof_log_item; pub mod qualified_contract; pub mod qualified_identity; +pub mod settings; pub mod wallet; diff --git a/src/model/qualified_identity/encrypted_key_storage.rs b/src/model/qualified_identity/encrypted_key_storage.rs index 94a48af6f..8bfe92ca1 100644 --- a/src/model/qualified_identity/encrypted_key_storage.rs +++ b/src/model/qualified_identity/encrypted_key_storage.rs @@ -5,10 +5,11 @@ use bincode::de::{BorrowDecoder, Decoder}; use bincode::enc::Encoder; use bincode::error::{DecodeError, EncodeError}; use bincode::{BorrowDecode, Decode, Encode}; -use dash_sdk::dashcore_rpc::dashcore::bip32::DerivationPath; -use dash_sdk::dpp::dashcore::bip32::ChildNumber; +use dash_sdk::dashcore_rpc::dashcore::Network; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dash_sdk::dpp::identity::{KeyID, Purpose, SecurityLevel}; +use dash_sdk::dpp::key_wallet::bip32::ChildNumber; +use dash_sdk::dpp::key_wallet::bip32::DerivationPath; use std::collections::{BTreeMap, BTreeSet}; use std::fmt; use std::sync::{Arc, RwLock}; @@ -277,6 +278,7 @@ impl KeyStorage { &self, key: &(PrivateKeyTarget, KeyID), wallets: &[Arc>], + network: Network, ) -> Result, String> { self.private_keys .get(key) @@ -296,6 +298,7 @@ impl KeyStorage { wallets, *wallet_seed_hash, derivation_path, + network, )? .ok_or(format!( "Wallet for key at derivation path {} not present, we have {} wallets", diff --git a/src/model/qualified_identity/mod.rs b/src/model/qualified_identity/mod.rs index 1e915e8b6..a09d8cbf8 100644 --- a/src/model/qualified_identity/mod.rs +++ b/src/model/qualified_identity/mod.rs @@ -8,7 +8,6 @@ use bincode::{Decode, Encode}; use dash_sdk::dashcore_rpc::dashcore::{PubkeyHash, signer}; use dash_sdk::dpp::bls_signatures::{Bls12381G2Impl, SignatureSchemes}; use dash_sdk::dpp::dashcore::address::Payload; -use dash_sdk::dpp::dashcore::bip32::ChildNumber; use dash_sdk::dpp::dashcore::hashes::Hash; use dash_sdk::dpp::dashcore::{Address, Network, ScriptHash}; use dash_sdk::dpp::data_contract::document_type::DocumentTypeRef; @@ -20,6 +19,7 @@ use dash_sdk::dpp::identity::hash::IdentityPublicKeyHashMethodsV0; use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dash_sdk::dpp::identity::signer::Signer; use dash_sdk::dpp::identity::{Identity, KeyID, KeyType, Purpose, SecurityLevel}; +use dash_sdk::dpp::key_wallet::bip32::ChildNumber; use dash_sdk::dpp::platform_value::BinaryData; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::dpp::state_transition::errors::InvalidIdentityPublicKeyTypeError; @@ -223,6 +223,13 @@ pub struct QualifiedIdentity { pub wallet_index: Option, pub top_ups: BTreeMap, pub status: IdentityStatus, + pub network: Network, +} + +impl AsRef for QualifiedIdentity { + fn as_ref(&self) -> &QualifiedIdentity { + self + } } impl PartialEq for QualifiedIdentity { @@ -280,6 +287,7 @@ impl Decode for QualifiedIdentity { wallet_index: None, top_ups: Default::default(), status: IdentityStatus::Unknown, // Loaded from the database, not encoded + network: Network::Dash, // Loaded from the database, not encoded }) } } @@ -302,6 +310,7 @@ impl Signer for QualifiedIdentity { .cloned() .collect::>() .as_slice(), + self.network, ) .map_err(ProtocolError::Generic)? .ok_or(ProtocolError::Generic(format!( @@ -610,21 +619,3 @@ impl QualifiedIdentity { Ok(wallet_info) } } -impl From for QualifiedIdentity { - fn from(value: Identity) -> Self { - QualifiedIdentity { - identity: value, - associated_voter_identity: None, - associated_operator_identity: None, - associated_owner_key_id: None, - identity_type: IdentityType::User, - alias: None, - private_keys: Default::default(), - dpns_names: vec![], - associated_wallets: BTreeMap::new(), - wallet_index: None, - top_ups: Default::default(), - status: IdentityStatus::Unknown, - } - } -} diff --git a/src/model/settings.rs b/src/model/settings.rs new file mode 100644 index 000000000..37b203bc7 --- /dev/null +++ b/src/model/settings.rs @@ -0,0 +1,109 @@ +use crate::model::password_info::PasswordInfo; +use crate::ui::RootScreenType; +use crate::ui::theme::ThemeMode; +use dash_sdk::dpp::dashcore::Network; +use std::path::PathBuf; + +/// Application settings structure +#[derive(Debug, Clone)] +pub struct Settings { + pub network: Network, + pub root_screen_type: RootScreenType, + pub password_info: Option, + /// Path to the Dash-Qt binary, if set. None means autodetect. + /// Empty value (`""`) means path deliberately not set, autodetect will not be performed. + pub dash_qt_path: Option, + pub overwrite_dash_conf: bool, + pub theme_mode: ThemeMode, +} + +impl + From<( + Network, + RootScreenType, + Option, + Option, + bool, + ThemeMode, + )> for Settings +{ + /// Converts a tuple into a Settings instance + /// + /// Used mainly for database operations where settings are retrieved as a tuple. + fn from( + tuple: ( + Network, + RootScreenType, + Option, + Option, + bool, + ThemeMode, + ), + ) -> Self { + Self::new(tuple.0, tuple.1, tuple.2, tuple.3, tuple.4, tuple.5) + } +} + +impl Default for Settings { + /// Default settings for the application + fn default() -> Self { + Self::new( + Network::Dash, + RootScreenType::RootScreenIdentities, + None, + None, // autodetect + true, + ThemeMode::System, + ) + } +} + +impl Settings { + /// Creates a new Settings instance + pub fn new( + network: Network, + root_screen_type: RootScreenType, + password_info: Option, + dash_qt_path: Option, + overwrite_dash_conf: bool, + theme_mode: ThemeMode, + ) -> Self { + Self { + network, + root_screen_type, + password_info, + dash_qt_path: dash_qt_path.or_else(detect_dash_qt_path), + overwrite_dash_conf, + theme_mode, + } + } +} + +/// Detects the path to the Dash-Qt binary on the system +fn detect_dash_qt_path() -> Option { + let path = which::which("dash-qt") + .map(|path| path.to_string_lossy().to_string()) + .inspect_err(|e| tracing::warn!("failed to find dash-qt: {}", e)) + .ok() + .map(PathBuf::from) + .unwrap_or_else(|| { + // Fallback to default paths based on the operating system + if cfg!(target_os = "macos") { + PathBuf::from("/Applications/Dash-Qt.app/Contents/MacOS/Dash-Qt") + } else if cfg!(target_os = "windows") { + // Retrieve the PROGRAMFILES environment variable or default to "C:\\Program Files" + let program_files = std::env::var("PROGRAMFILES") + .unwrap_or_else(|_| "C:\\Program Files".to_string()); + PathBuf::from(program_files).join("DashCore\\dash-qt.exe") + } else { + PathBuf::from("/usr/local/bin/dash-qt") // Default Linux path + } + }); + + if path.is_file() { + Some(path) + } else { + tracing::warn!("Dash-Qt binary not found at: {:?}", path); + None + } +} diff --git a/src/model/wallet/asset_lock_transaction.rs b/src/model/wallet/asset_lock_transaction.rs index 1e3265a06..ce8e13099 100644 --- a/src/model/wallet/asset_lock_transaction.rs +++ b/src/model/wallet/asset_lock_transaction.rs @@ -1,7 +1,6 @@ use crate::context::AppContext; use crate::model::wallet::Wallet; use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; -use dash_sdk::dpp::dashcore::psbt::serialize::Serialize; use dash_sdk::dpp::dashcore::secp256k1::Message; use dash_sdk::dpp::dashcore::sighash::SighashCache; use dash_sdk::dpp::dashcore::transaction::special_transaction::TransactionPayload; @@ -9,6 +8,7 @@ use dash_sdk::dpp::dashcore::transaction::special_transaction::asset_lock::Asset use dash_sdk::dpp::dashcore::{ Address, Network, OutPoint, PrivateKey, ScriptBuf, Transaction, TxIn, TxOut, }; +use dash_sdk::dpp::key_wallet::psbt::serialize::Serialize; use std::collections::BTreeMap; impl Wallet { diff --git a/src/model/wallet/encryption.rs b/src/model/wallet/encryption.rs index 0630945be..6a75a7ca0 100644 --- a/src/model/wallet/encryption.rs +++ b/src/model/wallet/encryption.rs @@ -1,5 +1,5 @@ use aes_gcm::aead::Aead; -use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; +use aes_gcm::{Aes256Gcm, KeyInit}; use argon2::{self, Argon2}; use bip39::rand::{RngCore, rngs::OsRng}; @@ -50,7 +50,7 @@ pub fn encrypt_message( // Encrypt the seed let encrypted_seed = cipher - .encrypt(Nonce::from_slice(&nonce), message) + .encrypt(nonce.as_slice().into(), message) .map_err(|e| e.to_string())?; Ok((encrypted_seed, salt, nonce)) @@ -85,10 +85,7 @@ impl ClosedKeyItem { // Decrypt the seed let seed = cipher - .decrypt( - Nonce::from_slice(&self.nonce), - self.encrypted_seed.as_slice(), - ) + .decrypt(self.nonce.as_slice().into(), self.encrypted_seed.as_slice()) .map_err(|e| e.to_string())?; let sized_seed = seed.try_into().map_err(|e: Vec| { diff --git a/src/model/wallet/mod.rs b/src/model/wallet/mod.rs index 5e3e5b912..52a1929c2 100644 --- a/src/model/wallet/mod.rs +++ b/src/model/wallet/mod.rs @@ -2,12 +2,12 @@ mod asset_lock_transaction; pub mod encryption; mod utxos; -use dash_sdk::dashcore_rpc::dashcore::bip32::{ChildNumber, ExtendedPubKey, KeyDerivationType}; +use dash_sdk::dpp::key_wallet::bip32::{ChildNumber, ExtendedPubKey, KeyDerivationType}; -use dash_sdk::dpp::dashcore::bip32::DerivationPath; use dash_sdk::dpp::dashcore::{ Address, InstantLock, Network, OutPoint, PrivateKey, PublicKey, Transaction, TxOut, }; +use dash_sdk::dpp::key_wallet::bip32::DerivationPath; use std::collections::{BTreeMap, HashMap}; use std::fmt::Debug; use std::ops::Range; @@ -336,6 +336,7 @@ impl Wallet { slice: &[Arc>], wallet_seed_hash: WalletSeedHash, derivation_path: &DerivationPath, + network: Network, ) -> Result, String> { for wallet in slice { // Attempt to read the wallet from the RwLock @@ -344,7 +345,7 @@ impl Wallet { if wallet_ref.seed_hash() == wallet_seed_hash { // Attempt to derive the private key using the provided derivation path let extended_private_key = derivation_path - .derive_priv_ecdsa_for_master_seed(wallet_ref.seed_bytes()?, Network::Dash) + .derive_priv_ecdsa_for_master_seed(wallet_ref.seed_bytes()?, network) .map_err(|e| e.to_string())?; return Ok(Some(extended_private_key.private_key.secret_bytes())); } @@ -356,9 +357,10 @@ impl Wallet { pub fn private_key_at_derivation_path( &self, derivation_path: &DerivationPath, + network: Network, ) -> Result { let extended_private_key = derivation_path - .derive_priv_ecdsa_for_master_seed(self.seed_bytes()?, Network::Dash) + .derive_priv_ecdsa_for_master_seed(self.seed_bytes()?, network) .map_err(|e| e.to_string())?; Ok(extended_private_key.to_priv()) } @@ -774,11 +776,11 @@ impl Wallet { context: &AppContext, ) -> Result<(), String> { // Check if the new balance differs from the current one. - if let Some(current_balance) = self.address_balances.get(address) { - if *current_balance == new_balance { - // If the balance hasn't changed, skip the update. - return Ok(()); - } + if let Some(current_balance) = self.address_balances.get(address) + && *current_balance == new_balance + { + // If the balance hasn't changed, skip the update. + return Ok(()); } // If there's no current balance or it has changed, update it. diff --git a/src/ui/components/amount_input.rs b/src/ui/components/amount_input.rs new file mode 100644 index 000000000..abc071145 --- /dev/null +++ b/src/ui/components/amount_input.rs @@ -0,0 +1,558 @@ +use crate::model::amount::Amount; +use crate::ui::components::{Component, ComponentResponse}; +use dash_sdk::dpp::balances::credits::MAX_CREDITS; +use dash_sdk::dpp::fee::Credits; +use egui::{InnerResponse, Response, TextEdit, Ui, Vec2, WidgetText}; + +/// Response from the amount input widget +#[derive(Clone)] +pub struct AmountInputResponse { + /// The response from the text edit widget + pub response: Response, + /// Whether the input text has changed + pub changed: bool, + /// The error message if the input is invalid + pub error_message: Option, + /// Whether the max button was clicked + pub max_clicked: bool, + /// The parsed amount if the input is valid (None for empty input or validation errors) + pub parsed_amount: Option, +} + +impl AmountInputResponse { + /// Returns whether the input is valid (no error message) + pub fn is_valid(&self) -> bool { + self.error_message.is_none() + } + + /// Returns whether the input has changed + pub fn has_changed(&self) -> bool { + self.changed + } +} + +impl ComponentResponse for AmountInputResponse { + type DomainType = Amount; + fn has_changed(&self) -> bool { + self.changed + } + + fn changed_value(&self) -> &Option { + &self.parsed_amount + } + + fn is_valid(&self) -> bool { + self.error_message.is_none() + } + + fn error_message(&self) -> Option<&str> { + self.error_message.as_deref() + } +} + +/// A reusable amount input widget that handles decimal parsing and validation. +/// This widget can be used for any type of amount input (tokens, Dash, etc.). +/// +/// The widget validates the input in real-time and shows error messages when +/// the input is invalid. It follows the component design pattern with lazy +/// initialization and response-based communication. +/// +/// # Usage +/// +/// Store the component as `Option` in your screen struct for lazy +/// initialization, then use the fluent builder API to configure it: +/// +/// ```rust,ignore +/// let amount_input = self.amount_input.get_or_insert_with(|| { +/// AmountInput::new(Amount::new_dash(0.0)) +/// .label("Amount:") +/// .hint_text("Enter amount") +/// .max_amount(Some(1000000)) +/// .min_amount(Some(1000)) +/// .max_button(true) +/// }); +/// +/// let response = amount_input.show(ui); +/// response.inner.update(&mut self.amount); +/// ``` +/// +/// See the tests for complete usage examples. +pub struct AmountInput { + // Raw data, as entered by the user + amount_str: String, + decimal_places: u8, + unit_name: Option, + label: Option, + hint_text: Option, + max_amount: Option, + min_amount: Option, + show_max_button: bool, + desired_width: Option, + show_validation_errors: bool, + // When true, we enforce that the input was changed, even if text edit didn't change. + changed: bool, +} + +impl AmountInput { + /// Creates a new amount input widget from an Amount. + /// + /// # Arguments + /// * `amount` - The initial amount to display (determines decimal places automatically) + /// + /// The decimal places are automatically set based on the Amount object. + /// Amount entered by the user will be available through [`AmountInputResponse`]. + pub fn new>(amount: T) -> Self { + let amount = amount.as_ref(); + let amount_str = if amount.value() == 0 { + String::new() + } else { + amount.to_string_without_unit() + }; + Self { + amount_str, + decimal_places: amount.decimal_places(), + unit_name: amount.unit_name().map(|s| s.to_string()), + label: None, + hint_text: None, + max_amount: Some(MAX_CREDITS), + min_amount: Some(1), // Default minimum is 1 (greater than zero) + show_max_button: false, + desired_width: None, + show_validation_errors: true, // Default to showing validation errors + changed: true, // Start as changed to force initial validation + } + } + + /// Sets whether the input has changed. + /// This is useful for cases where you want to force the component to treat the input as changed, + /// even if the text edit widget itself did not register a change. + pub fn set_changed(&mut self, changed: bool) -> &mut Self { + self.changed = changed; + self + } + + /// Gets the number of decimal places this input is configured for. + pub fn decimal_places(&self) -> u8 { + self.decimal_places + } + + /// Update decimal places used to render values. + /// + /// Value displayed in the input is not changed, but the actual [Amount] + /// will be multiplied or divided by 10^(difference of decimal places). + /// + /// ## Example + /// + /// The input contains `12.34` and decimal places is set to 3. + /// It will be interpreted as `12.340` when parsed (credits value `12_340`). + /// + /// + /// If you change the decimal places from 3 to 5: + /// + /// * The input will still display `12.34` (unchanged) + /// * The next time the input is parsed, it will generate `12.34000` + /// (credits value `1_234_000`). + pub fn set_decimal_places(&mut self, decimal_places: u8) -> &mut Self { + self.decimal_places = decimal_places; + self.changed = true; + + self + } + + /// Gets the unit name this input is configured for. + pub fn unit_name(&self) -> Option<&str> { + self.unit_name.as_deref() + } + + /// Sets the label for the input field. + pub fn with_label>(mut self, label: T) -> Self { + self.label = Some(label.into()); + self + } + + /// Sets the label for the input field (mutable reference version). + /// Use this for dynamic configuration when the label needs to change after initialization. + pub fn set_label>(&mut self, label: T) -> &mut Self { + self.label = Some(label.into()); + self + } + + /// Sets value of the input field. + /// + /// This will update the internal state and mark the component as changed. + pub fn set_value(&mut self, value: Amount) -> &mut Self { + self.amount_str = value.to_string_without_unit(); + self.decimal_places = value.decimal_places(); + self.unit_name = value.unit_name().map(|s| s.to_string()); + self.changed = true; // Mark as changed to trigger validation + self + } + + /// Sets the hint text for the input field. + pub fn with_hint_text>(mut self, hint_text: T) -> Self { + self.hint_text = Some(hint_text.into()); + self + } + + /// Sets the hint text for the input field (mutable reference version). + pub fn set_hint_text>(&mut self, hint_text: T) -> &mut Self { + self.hint_text = Some(hint_text.into()); + self + } + + /// Sets the maximum amount allowed. If provided, a "Max" button will be shown + /// when `show_max_button` is true. + pub fn with_max_amount(mut self, max_amount: Option) -> Self { + self.max_amount = max_amount; + self + } + + /// Sets the maximum amount allowed (mutable reference version). + /// Use this for dynamic configuration when the max amount changes at runtime (e.g., balance updates). + /// + /// Defaults to [`MAX_CREDITS`](dash_sdk::dpp::balances::credits::MAX_CREDITS). + pub fn set_max_amount(&mut self, max_amount: Option) -> &mut Self { + self.max_amount = max_amount; + self + } + + /// Sets the minimum amount allowed. Defaults to 1 (must be greater than zero). + /// Set to Some(0) to allow zero amounts, or None to disable minimum validation. + pub fn with_min_amount(mut self, min_amount: Option) -> Self { + self.min_amount = min_amount; + self + } + + /// Sets the minimum amount allowed (mutable reference version). + pub fn set_min_amount(&mut self, min_amount: Option) -> &mut Self { + self.min_amount = min_amount; + self + } + + /// Whether to show a "Max" button that sets the amount to the maximum. + pub fn with_max_button(mut self, show: bool) -> Self { + self.show_max_button = show; + self + } + + /// Whether to show a "Max" button (mutable reference version). + pub fn set_show_max_button(&mut self, show: bool) -> &mut Self { + self.show_max_button = show; + self + } + + /// Sets the desired width of the input field. + pub fn with_desired_width(mut self, width: f32) -> Self { + self.desired_width = Some(width); + self + } + + /// Sets the desired width of the input field (mutable reference version). + pub fn set_desired_width(&mut self, width: f32) -> &mut Self { + self.desired_width = Some(width); + self + } + + /// Controls whether validation errors are displayed as a label within the component. + pub fn show_validation_errors(mut self, show: bool) -> Self { + self.show_validation_errors = show; + self + } + + /// Validates the current amount string and returns validation results. + /// + /// Returns `Ok(Some(Amount))` for valid input, `Ok(None)` for empty input, + /// or `Err(String)` with error message if validation fails. + fn validate_amount(&self) -> Result, String> { + if self.amount_str.trim().is_empty() { + return Ok(None); + } + + match Amount::parse(&self.amount_str, self.decimal_places) { + Ok(mut amount) => { + // Apply the unit name if we have one + if let Some(ref unit_name) = self.unit_name { + amount = amount.with_unit_name(unit_name); + } + + // Check if amount exceeds maximum + if let Some(max_amount) = self.max_amount + && amount.value() > max_amount + { + return Err(format!( + "Amount {} exceeds allowed maximum {}", + amount, + Amount::new(max_amount, self.decimal_places) + )); + } + + // Check if amount is below minimum + if let Some(min_amount) = self.min_amount + && amount.value() < min_amount + { + return Err(format!( + "Amount must be at least {}", + Amount::new(min_amount, self.decimal_places) + )); + } + + Ok(Some(amount)) + } + Err(error) => Err(error), + } + } + + /// Renders the amount input widget and returns an `InnerResponse` for use with `show()`. + fn show_internal(&mut self, ui: &mut Ui) -> InnerResponse { + ui.horizontal(|ui| { + if self.show_max_button { + // ensure we have height predefined to correctly vertically align the input field; + // see StyledButton::show() to see how y is calculated + ui.allocate_space(Vec2::new(0.0, 30.0)); + } + // Show label if provided + if let Some(label) = &self.label { + ui.label(label.clone()); + } + // Create the text edit widget + let mut text_edit = TextEdit::singleline(&mut self.amount_str); + + if let Some(hint) = &self.hint_text { + text_edit = text_edit.hint_text(hint.clone()); + } + + if let Some(width) = self.desired_width { + text_edit = text_edit.desired_width(width); + } + + let text_response = ui.add(text_edit); + + let mut changed = text_response.changed() && ui.is_enabled(); + + // Show max button if max amount is available + let mut max_clicked = false; + if self.show_max_button { + if let Some(max_amount) = self.max_amount { + if ui.button("Max").clicked() { + self.amount_str = Amount::new(max_amount, self.decimal_places).to_string(); + max_clicked = true; + changed = true; + } + } else if ui.button("Max").clicked() { + // Max button clicked but no max amount set - still report the click + max_clicked = true; + } + } + + // Validate the amount + let (error_message, parsed_amount) = match self.validate_amount() { + Ok(amount) => (None, amount), + Err(error) => (Some(error), None), + }; + + // Show validation error if enabled and error exists + if self.show_validation_errors + && let Some(error_msg) = &error_message + { + ui.colored_label(ui.visuals().error_fg_color, error_msg); + } + + if self.changed { + changed = true; // Force changed if set + self.changed = false; // Reset after use + } + + AmountInputResponse { + response: text_response, + changed, + error_message, + max_clicked, + parsed_amount, + } + }) + } +} + +impl Component for AmountInput { + type DomainType = Amount; + type Response = AmountInputResponse; + + fn show(&mut self, ui: &mut Ui) -> InnerResponse { + AmountInput::show_internal(self, ui) + } + + fn current_value(&self) -> Option { + // Validate the current amount string and return the parsed amount + match self.validate_amount() { + Ok(Some(amount)) => Some(amount), + Ok(None) => None, // Empty input + Err(_) => None, // Invalid input returns None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_initialization_with_non_zero_amount_and_unit() { + // Test that AmountInput correctly initializes from an existing amount + let amount = Amount::new_dash(1.5); // 1.5 DASH + + assert_eq!(amount.unit_name(), Some("DASH")); + assert_eq!(format!("{}", amount), "1.5 DASH"); + + let amount_input = AmountInput::new(amount); + + // The amount_str should be initialized with the numeric part, not the unit + assert_eq!(amount_input.amount_str, "1.5"); + assert_eq!(amount_input.decimal_places, 11); + } + + #[test] + fn test_initialization_with_zero_amount() { + // Test that zero amounts initialize with empty string + let amount = Amount::new_dash(0.0); + let amount_input = AmountInput::new(amount); + assert_eq!(amount_input.amount_str, ""); + assert_eq!(amount_input.decimal_places, 11); + } + + #[test] + fn test_minimum_amount_settings() { + let amount = Amount::new(0, 8); // Generic amount with 8 decimal places + + // Default minimum should be 1 + let input = AmountInput::new(amount); + assert_eq!(input.min_amount, Some(1)); + + // Custom minimum + let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(Some(1000)); + assert_eq!(input.min_amount, Some(1000)); + + // Allow zero + let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(Some(0)); + assert_eq!(input.min_amount, Some(0)); + + // No minimum + let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(None); + assert_eq!(input.min_amount, None); + } + + #[test] + fn test_unit_name_preservation() { + let amount = Amount::new(150_000_000_000, 11).with_unit_name("DASH"); // 1.5 DASH + let mut input = AmountInput::new(amount); + + // Check that unit name is preserved + assert_eq!(input.unit_name(), Some("DASH")); + + // Test that get_current_amount preserves unit name + input.amount_str = "2.5".to_string(); + let current = input.validate_amount().unwrap().unwrap(); + assert_eq!(current.unit_name(), Some("DASH")); + assert_eq!(format!("{}", current), "2.5 DASH"); + + // Test validation also preserves unit name + let validation_result = input.validate_amount(); + assert!(validation_result.is_ok()); + let parsed = validation_result.unwrap().unwrap(); + assert_eq!(parsed.unit_name(), Some("DASH")); + assert_eq!(format!("{}", parsed), "2.5 DASH"); + } + + #[test] + fn test_token_unit_name_preservation() { + let amount = Amount::new(1000000, 6).with_unit_name("MYTOKEN"); // 1.0 MYTOKEN + let mut input = AmountInput::new(amount); + + // Check that token unit name is preserved + assert_eq!(input.unit_name(), Some("MYTOKEN")); + + // Test with different amount + input.amount_str = "5.5".to_string(); + let current = input.validate_amount().unwrap().unwrap(); + assert_eq!(current.unit_name(), Some("MYTOKEN")); + assert_eq!(format!("{}", current), "5.5 MYTOKEN"); + } + + #[test] + fn test_validation_states() { + let amount = Amount::new(0, 2); // 2 decimal places for simple testing + let mut input = AmountInput::new(amount); + + // Test empty input (valid) + input.amount_str = "".to_string(); + let validation_result = input.validate_amount(); + assert!(validation_result.is_ok(), "Empty input should be valid"); + assert!( + validation_result.unwrap().is_none(), + "Empty input should have no parsed amount" + ); + + // Test valid input + input.amount_str = "10.50".to_string(); + let validation_result = input.validate_amount(); + assert!( + validation_result.is_ok(), + "Valid input should have no error" + ); + assert!( + validation_result.unwrap().is_some(), + "Valid input should have parsed amount" + ); + + // Test invalid input (too many decimals) + input.amount_str = "10.555".to_string(); + let validation_result = input.validate_amount(); + assert!( + validation_result.is_err(), + "Invalid input should have error" + ); + + // Test invalid input (non-numeric) + input.amount_str = "abc".to_string(); + let validation_result = input.validate_amount(); + assert!( + validation_result.is_err(), + "Non-numeric input should have error" + ); + } + + #[test] + fn test_min_max_validation() { + let amount = Amount::new(0, 2); + let mut input = AmountInput::new(amount) + .with_min_amount(Some(100)) // Minimum 1.00 + .with_max_amount(Some(10000)); // Maximum 100.00 + + // Test amount below minimum + input.amount_str = "0.50".to_string(); // 50 (below min of 100) + let validation_result = input.validate_amount(); + assert!( + validation_result.is_err(), + "Amount below minimum should have error" + ); + + // Test amount above maximum + input.amount_str = "150.00".to_string(); // 15000 (above max of 10000) + let validation_result = input.validate_amount(); + assert!( + validation_result.is_err(), + "Amount above maximum should have error" + ); + + // Test valid amount within range + input.amount_str = "50.00".to_string(); // 5000 (within range) + let validation_result = input.validate_amount(); + assert!( + validation_result.is_ok(), + "Amount within range should have no error" + ); + assert!( + validation_result.unwrap().is_some(), + "Amount within range should have parsed amount" + ); + } +} diff --git a/src/ui/components/component_trait.rs b/src/ui/components/component_trait.rs new file mode 100644 index 000000000..6a5feb28e --- /dev/null +++ b/src/ui/components/component_trait.rs @@ -0,0 +1,103 @@ +use egui::{InnerResponse, Ui}; + +/// Generic response trait for all UI components following the design pattern. +/// +/// All component responses should implement this trait to provide consistent +/// access to basic response properties. +pub trait ComponentResponse: Clone { + /// The domain object type that this response represents. + /// This type represents the data this component is designed to handle, + /// such as Amount, Identity, etc. + /// + /// It must be equal to the `DomainType` of the component that produced this response. + type DomainType; + + /// Returns whether the component input/state has changed + fn has_changed(&self) -> bool; + + /// Returns whether the component is in a valid state (no error) + fn is_valid(&self) -> bool; + + /// Returns the changed value of the component, if any; otherwise, `None`. + /// It is Some() only if `has_changed()` is true. + /// + /// Note that only valid values should be returned here. + /// If the component value is invalid, this should return `None`. + fn changed_value(&self) -> &Option; + + /// Returns any error message from the component + fn error_message(&self) -> Option<&str>; + + /// Binds the response to a mutable value, updating it if the component state has changed. + /// + /// Provided `value` will be updated whenever the user changes the component state. + /// It will be set to `None` if the component state is invalid (eg. user entered value that didn't pass the validation). + /// + /// # Returns + /// + /// * `true` if the value was updated (including change to `None`), + /// * `false` if it was not changed (eg. `self.has_changed() == false`). + fn update(&self, value: &mut Option) -> bool + where + Self::DomainType: Clone, + { + if self.has_changed() { + if let Some(inner) = self.changed_value() { + value.replace(inner.clone()); + true + } else { + value.take(); + true + } + } else { + false + } + } +} + +/// Core trait that all UI components following the design pattern should implement. +/// +/// This trait provides a standardized interface for components that follow the +/// established patterns of lazy initialization, dual configuration APIs, and +/// response-based communication. +/// +/// # Type Parameters +/// +/// * `DomainType` - The domain object type that this component is designed to handle. +/// This represents the conceptual data type the component works with (e.g., Amount, Identity). +/// * `Response` - The specific response type returned by the component's `show()` method +/// +/// # See also +/// +/// See `doc/COMPONENT_DESIGN_PATTERN.md` for detailed design pattern documentation. +pub trait Component { + /// The domain object type that this component is designed to handle. + /// This type represents the data this component is designed to handle, + /// such as Amount, Identity, etc. + type DomainType; + + /// The response type returned by the component's `show()` method. + /// This type should implement `ComponentResponse` and contain all + /// information about the component's current state and any changes. + type Response: ComponentResponse; + + /// Renders the component and returns a response with interaction results. + /// + /// This method should handle both rendering the component and processing + /// any user interactions, including validation, error display, hints, + /// and formatting. + /// + /// # Returns + /// + /// An [`InnerResponse`] containing the component's response data in [`InnerResponse::inner`] field. + /// [`InnerResponse::inner`] should implement [`ComponentResponse`] trait. + fn show(&mut self, ui: &mut Ui) -> InnerResponse; + + /// Returns the current value of the component. + /// + /// Note that only valid values should be returned here. + /// If the component value is invalid, this should return `None`. + /// + /// See [`ComponentResponse::current_value`] for more details. + fn current_value(&self) -> Option; +} diff --git a/src/ui/components/confirmation_dialog.rs b/src/ui/components/confirmation_dialog.rs new file mode 100644 index 000000000..6f04f2a2b --- /dev/null +++ b/src/ui/components/confirmation_dialog.rs @@ -0,0 +1,359 @@ +use std::sync::Arc; + +use crate::ui::components::component_trait::{Component, ComponentResponse}; +use crate::ui::theme::{ComponentStyles, DashColors, Shape}; +use egui::{InnerResponse, Ui, WidgetText}; + +/// Response from showing a confirmation dialog +#[derive(Debug, Clone, PartialEq)] +pub enum ConfirmationStatus { + /// User clicked confirm button + Confirmed, + /// User clicked cancel button or closed dialog + Canceled, +} + +pub const NOTHING: Option<&str> = None; +/// Response struct for the ConfirmationDialog component following the Component trait pattern +#[derive(Debug, Clone)] +pub struct ConfirmationDialogComponentResponse { + pub response: egui::Response, + pub changed: bool, + pub error_message: Option, + pub dialog_response: Option, +} + +impl ComponentResponse for ConfirmationDialogComponentResponse { + type DomainType = ConfirmationStatus; + + fn has_changed(&self) -> bool { + self.changed + } + + fn is_valid(&self) -> bool { + self.error_message.is_none() + } + + fn changed_value(&self) -> &Option { + if self.has_changed() { + &self.dialog_response + } else { + &None + } + } + + fn error_message(&self) -> Option<&str> { + self.error_message.as_deref() + } +} +/// A reusable confirmation dialog component that implements the Component trait +/// +/// This component provides a consistent modal dialog for confirming user actions +/// across the application. It supports customizable titles, messages, button text +/// with rich formatting (using WidgetText for styling), danger mode for destructive +/// actions, and optional buttons (confirm and cancel buttons can be hidden independently). +/// The dialog can be dismissed by pressing Escape (treated as cancel) or clicking the X button. +pub struct ConfirmationDialog { + title: WidgetText, + message: WidgetText, + status: Option, + confirm_text: Option, + cancel_text: Option, + danger_mode: bool, + is_open: bool, +} + +impl Component for ConfirmationDialog { + type DomainType = ConfirmationStatus; + type Response = ConfirmationDialogComponentResponse; + + fn show(&mut self, ui: &mut Ui) -> InnerResponse { + let inner_response = self.show_dialog(ui); + let changed = inner_response.inner.is_some(); + let response = inner_response.response; + + InnerResponse::new( + ConfirmationDialogComponentResponse { + response: response.clone(), + changed, + error_message: None, // Confirmation dialogs don't have validation errors + dialog_response: inner_response.inner, + }, + response, + ) + } + + fn current_value(&self) -> Option { + // Return the current dialog state - None if still open, Some(status) if closed + if self.is_open { + None + } else { + Some(ConfirmationStatus::Canceled) // If dialog is closed, it was canceled + } + } +} + +impl ConfirmationDialog { + /// Create a new confirmation dialog with the given title and message + pub fn new(title: impl Into, message: impl Into) -> Self { + Self { + title: title.into(), + message: message.into(), + confirm_text: Some("Confirm".into()), + cancel_text: Some("Cancel".into()), + danger_mode: false, + is_open: true, + status: None, // No action taken yet + } + } + + /// Set the text for the confirm button, or None to hide it + pub fn confirm_text(mut self, text: Option>) -> Self { + self.confirm_text = text.map(|t| t.into()); + self + } + + /// Set the text for the cancel button, or None to hide it + pub fn cancel_text(mut self, text: Option>) -> Self { + self.cancel_text = text.map(|t| t.into()); + self + } + + /// Enable danger mode (red confirm button) for destructive actions + pub fn danger_mode(mut self, enabled: bool) -> Self { + self.danger_mode = enabled; + self + } + + /// Set whether the dialog is open + pub fn open(mut self, open: bool) -> Self { + self.is_open = open; + self + } +} + +impl ConfirmationDialog { + /// Show the dialog and return the user's response + fn show_dialog(&mut self, ui: &mut Ui) -> InnerResponse> { + let mut is_open = self.is_open; + + if !is_open { + return InnerResponse::new( + None, // no change + ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()), + ); + } + + // Draw dark overlay behind the dialog for better visibility + let screen_rect = ui.ctx().screen_rect(); + let painter = ui.ctx().layer_painter(egui::LayerId::new( + egui::Order::Background, + egui::Id::new("confirmation_dialog_overlay"), + )); + painter.rect_filled( + screen_rect, + 0.0, + egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120), // Semi-transparent black overlay + ); + + let mut final_response = None; + let window_response = egui::Window::new(self.title.clone()) + .collapsible(false) + .resizable(false) + .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) + .open(&mut is_open) + .frame(egui::Frame { + inner_margin: egui::Margin::same(16), + outer_margin: egui::Margin::same(0), + corner_radius: egui::CornerRadius::same(8), + shadow: egui::epaint::Shadow { + offset: [0, 8], + blur: 16, + spread: 0, + color: egui::Color32::from_rgba_unmultiplied(0, 0, 0, 100), + }, + fill: ui.style().visuals.window_fill, + stroke: egui::Stroke::new( + 1.0, + egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30), + ), + }) + .show(ui.ctx(), |ui| { + // Set minimum width for the dialog + ui.set_min_width(300.0); + + let dark_mode = ui.ctx().style().visuals.dark_mode; + + // Message content with bold text and proper color + ui.add_space(10.0); + ui.label( + egui::RichText::new(self.message.text()) + .strong() + .color(DashColors::text_primary(dark_mode)), + ); + ui.add_space(20.0); + + // Buttons + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Confirm button (only if text is provided) + if let Some(confirm_text) = &self.confirm_text { + let (fill_color, text_color) = if self.danger_mode { + ( + ComponentStyles::danger_button_fill(), + ComponentStyles::danger_button_text(), + ) + } else { + ( + ComponentStyles::primary_button_fill(), + ComponentStyles::primary_button_text(), + ) + }; + let confirm_label = if let WidgetText::RichText(rich_text) = + confirm_text + { + // preserve rich text formatting + rich_text.clone() + } else { + Arc::new(egui::RichText::new(confirm_text.text()).color(text_color)) + }; + + let confirm_button = egui::Button::new(confirm_label) + .fill(fill_color) + .stroke(if self.danger_mode { + egui::Stroke::NONE + } else { + ComponentStyles::primary_button_stroke() + }) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) + .min_size(egui::Vec2::new(80.0, 32.0)); + + if ui + .add(confirm_button) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() + { + final_response = Some(ConfirmationStatus::Confirmed); + } + } + + // Cancel button (only if text is provided) + if let Some(cancel_text) = &self.cancel_text { + let cancel_label = if let WidgetText::RichText(rich_text) = cancel_text + { + // preserve rich text formatting + rich_text.clone() + } else { + egui::RichText::new(cancel_text.text()) + .color(ComponentStyles::secondary_button_text()) + .into() + }; + + let cancel_button = egui::Button::new(cancel_label) + .fill(ComponentStyles::secondary_button_fill()) + .stroke(ComponentStyles::secondary_button_stroke()) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) + .min_size(egui::Vec2::new(80.0, 32.0)); + + if ui + .add(cancel_button) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() + { + final_response = Some(ConfirmationStatus::Canceled); + } + + ui.add_space(8.0); // Add spacing between buttons + } + }); + }); + }); + + // Handle window being closed via X button - treat as cancel + if !is_open && final_response.is_none() { + final_response = Some(ConfirmationStatus::Canceled); + } + + // Handle Escape key press - always treat as cancel + if final_response.is_none() && ui.input(|i| i.key_pressed(egui::Key::Escape)) { + final_response = Some(ConfirmationStatus::Canceled); + } + + // Update the dialog's state + self.is_open = is_open; + // if user actually did something, update the status + if final_response.is_some() { + self.status = final_response.clone(); + } + + if let Some(window_response) = window_response { + InnerResponse::new(final_response, window_response.response) + } else { + InnerResponse::new( + final_response, + ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover()), + ) + } + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_confirmation_dialog_creation() { + let dialog = ConfirmationDialog::new("Test Title", "Test Message") + .confirm_text(Some("Yes")) + .cancel_text(Some("No")) + .danger_mode(true); + + assert_eq!(dialog.title.text(), "Test Title"); + assert_eq!(dialog.message.text(), "Test Message"); + assert!(dialog.confirm_text.is_some_and(|t| t.text() == "Yes")); + assert!(dialog.cancel_text.is_some_and(|t| t.text() == "No")); + assert!(dialog.danger_mode); + assert!(dialog.is_open); + } + + #[test] + fn test_confirmation_dialog_no_buttons() { + let dialog = ConfirmationDialog::new("Test Title", "Test Message") + .confirm_text(NOTHING) + .cancel_text(NOTHING); + + assert_eq!(dialog.title.text(), "Test Title"); + assert_eq!(dialog.message.text(), "Test Message"); + assert!(dialog.confirm_text.is_none()); + assert!(dialog.cancel_text.is_none()); + assert!(!dialog.danger_mode); + assert!(dialog.is_open); + } + + #[test] + fn test_confirmation_dialog_only_confirm_button() { + let dialog = ConfirmationDialog::new("Test Title", "Test Message") + .confirm_text(Some("OK")) + .cancel_text(NOTHING); + + assert_eq!(dialog.title.text(), "Test Title"); + assert_eq!(dialog.message.text(), "Test Message"); + assert!(dialog.confirm_text.is_some()); + assert!(dialog.cancel_text.is_none()); + assert!(!dialog.danger_mode); + assert!(dialog.is_open); + } + + #[test] + fn test_confirmation_dialog_only_cancel_button() { + let dialog = ConfirmationDialog::new("Test Title", "Test Message") + .confirm_text(NOTHING) + .cancel_text(Some("Close")); + + assert_eq!(dialog.title.text(), "Test Title"); + assert_eq!(dialog.message.text(), "Test Message"); + assert!(dialog.confirm_text.is_none()); + assert!(dialog.cancel_text.is_some()); + assert!(!dialog.danger_mode); + assert!(dialog.is_open); + } +} diff --git a/src/ui/components/contract_chooser_panel.rs b/src/ui/components/contract_chooser_panel.rs index 08f00648d..b5c8add4a 100644 --- a/src/ui/components/contract_chooser_panel.rs +++ b/src/ui/components/contract_chooser_panel.rs @@ -3,7 +3,6 @@ use crate::backend_task::BackendTask; use crate::backend_task::contract::ContractTask; use crate::context::AppContext; use crate::model::qualified_contract::QualifiedContract; -use crate::ui::components::styled::ClickableCollapsingHeader; use crate::ui::contracts_documents::contracts_documents_screen::DOCUMENT_PRIVATE_FIELDS; use crate::ui::theme::{DashColors, Shadow, Shape, Spacing}; use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; @@ -24,6 +23,11 @@ pub struct ContractChooserState { pub right_click_contract_id: Option, pub show_context_menu: bool, pub context_menu_position: egui::Pos2, + pub expanded_contracts: std::collections::HashSet, + pub expanded_sections: std::collections::HashMap>, + pub expanded_doc_types: std::collections::HashMap>, + pub expanded_indexes: std::collections::HashMap>, + pub expanded_tokens: std::collections::HashMap>, } impl Default for ContractChooserState { @@ -32,10 +36,112 @@ impl Default for ContractChooserState { right_click_contract_id: None, show_context_menu: false, context_menu_position: egui::Pos2::ZERO, + expanded_contracts: std::collections::HashSet::new(), + expanded_sections: std::collections::HashMap::new(), + expanded_doc_types: std::collections::HashMap::new(), + expanded_indexes: std::collections::HashMap::new(), + expanded_tokens: std::collections::HashMap::new(), } } } +// Helper function to render a custom collapsing header with +/- button +fn render_collapsing_header( + ui: &mut egui::Ui, + text: impl Into, + is_expanded: bool, + is_selected: bool, + indent_level: usize, +) -> bool { + let text = text.into(); + let dark_mode = ui.ctx().style().visuals.dark_mode; + let indent = indent_level as f32 * 16.0; + + let mut clicked = false; + + ui.horizontal(|ui| { + ui.add_space(indent); + + // +/- button + let button_text = if is_expanded { "−" } else { "+" }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + + if button_response.clicked() { + clicked = true; + } + + // Label - make contract names (level 0) larger + let label_text = if indent_level == 0 { + // Contract names - make them the largest with heading font + if is_selected { + RichText::new(text) + .size(16.0) + .heading() + .color(DashColors::DASH_BLUE) + } else { + RichText::new(text) + .size(16.0) + .heading() + .color(DashColors::text_primary(dark_mode)) + } + } else if indent_level == 1 { + // Section headers (Document Types, Tokens, Contract JSON) - medium size + if is_selected { + RichText::new(text) + .size(14.0) + .heading() + .color(DashColors::DASH_BLUE) + } else { + RichText::new(text) + .size(14.0) + .heading() + .color(DashColors::text_primary(dark_mode)) + } + } else if indent_level == 2 { + // Document type names - smaller + if is_selected { + RichText::new(text) + .size(13.0) + .heading() + .color(DashColors::DASH_BLUE) + } else { + RichText::new(text) + .size(13.0) + .heading() + .color(DashColors::text_primary(dark_mode)) + } + } else { + // Indexes and other sub-items - smallest + if is_selected { + RichText::new(text) + .size(12.0) + .heading() + .color(DashColors::DASH_BLUE) + } else { + RichText::new(text) + .size(12.0) + .heading() + .color(DashColors::text_primary(dark_mode)) + } + }; + + let label_response = ui.add(egui::Label::new(label_text).sense(egui::Sense::click())); + if label_response.clicked() { + clicked = true; + } + }); + + clicked +} + #[allow(clippy::too_many_arguments)] pub fn add_contract_chooser_panel( ctx: &EguiContext, @@ -76,7 +182,7 @@ pub fn add_contract_chooser_panel( SidePanel::left("contract_chooser_panel") // Let the user resize this panel horizontally .resizable(true) - .default_width(270.0) // Increased to account for margins + .default_width(270.0) .frame( Frame::new() .fill(DashColors::background(dark_mode)) @@ -106,382 +212,327 @@ pub fn add_contract_chooser_panel( }); // List out each matching contract - ui.vertical(|ui| { + ui.vertical_centered(|ui| { + ui.spacing_mut().item_spacing.y = 0.0; // Remove vertical spacing between contracts + for contract in filtered_contracts { - ui.push_id( - contract.contract.id().to_string(Encoding::Base58), - |ui| { - ui.horizontal(|ui| { - let is_selected_contract = - *selected_data_contract == *contract; - - let name_or_id = contract.alias.clone().unwrap_or( - contract.contract.id().to_string(Encoding::Base58), - ); - - // Highlight the contract if selected - let contract_header_text = if is_selected_contract { - RichText::new(name_or_id) - .color(Color32::from_rgb(21, 101, 192)) - } else { - RichText::new(name_or_id) - }; - - // Expand/collapse the contract info - let contract_id = contract.contract.id().to_string(Encoding::Base58); - let collapsing_response = - ClickableCollapsingHeader::new(contract_header_text.text().to_string()) - .id_salt(format!("contract_{}", contract_id)) - .show(ui, |ui| { - // - // ===== Document Types Section ===== - // - ClickableCollapsingHeader::new("Document Types") - .id_salt(format!("contract_{}_doc_types", contract_id)) - .show(ui, |ui| { - for (doc_name, doc_type) in - contract.contract.document_types() - { - let is_selected_doc_type = - *selected_document_type - == *doc_type; - - let doc_type_header_text = - if is_selected_doc_type { - RichText::new(doc_name.clone()) - .color(Color32::from_rgb( - 21, 101, 192, - )) - } else { - RichText::new(doc_name.clone()) - }; - - let doc_resp = - ClickableCollapsingHeader::new(doc_type_header_text.text().to_string()) - .id_salt(format!("contract_{}_doc_{}", contract_id, doc_name)) - .show(ui, |ui| { - // Show the indexes - if doc_type.indexes().is_empty() { - ui.label("No indexes defined"); - } else { - for (index_name, index) in - doc_type.indexes() - { - let is_selected_index = *selected_index - == Some(index.clone()); - - let index_header_text = - if is_selected_index { - RichText::new(format!( - "Index: {}", - index_name - )) - .color(Color32::from_rgb( - 21, 101, 192, - )) - } else { - RichText::new(format!( - "Index: {}", - index_name - )) - }; - - let index_resp = ClickableCollapsingHeader::new(index_header_text.text().to_string()) - .id_salt(format!("contract_{}_doc_{}_index_{}", contract_id, doc_name, index_name)) - .show(ui, |ui| { - // Show index properties if expanded - for prop in &index.properties { - ui.label(format!( - "{:?}", - prop - )); - } - }); + let contract_id = contract.contract.id().to_string(Encoding::Base58); + let is_selected_contract = *selected_data_contract == *contract; + + // Format built-in contract names nicely + let display_name = match contract.alias.as_deref() { + Some("dpns") => "DPNS".to_string(), + Some("keyword_search") => "Keyword Search".to_string(), + Some("token_history") => "Token History".to_string(), + Some("withdrawals") => "Withdrawals".to_string(), + Some(alias) => alias.to_string(), + None => contract_id.clone(), + }; + + // Check if this contract is expanded + let is_expanded = chooser_state.expanded_contracts.contains(&contract_id); + + // Render the custom collapsing header for the contract + if render_collapsing_header(ui, &display_name, is_expanded, is_selected_contract, 0) { + if is_expanded { + chooser_state.expanded_contracts.remove(&contract_id); + } else { + chooser_state.expanded_contracts.insert(contract_id.clone()); + } + } - // If index was just clicked (opened) - if index_resp.header_response.clicked() - && index_resp - .body_response - .is_some() - { - *selected_index = - Some(index.clone()); - if let Ok(new_doc_type) = contract - .contract - .document_type_cloned_for_name( - doc_name, - ) - { - *selected_document_type = - new_doc_type; - *selected_data_contract = - contract.clone(); - - // Build the WHERE clause using all property names - let conditions: Vec = - index - .property_names() - .iter() - .map(|property_name| { - format!( - "`{}` = '___'", - property_name - ) - }) - .collect(); - - let where_clause = - if conditions.is_empty() { - String::new() - } else { - format!( - " WHERE {}", - conditions - .join(" AND ") - ) - }; - - *document_query = format!( - "SELECT * FROM {}{}", - selected_document_type - .name(), - where_clause - ); - } - } - // If index was just collapsed - else if index_resp - .header_response - .clicked() - && index_resp - .body_response - .is_none() - { - *selected_index = None; - *document_query = format!( - "SELECT * FROM {}", - selected_document_type.name() - ); - } - } + // Show contract content if expanded + if is_expanded { + ui.push_id(&contract_id, |ui| { + ui.vertical(|ui| { + // + // ===== Document Types Section ===== + // + // Only show Document Types section if there are document types + if !contract.contract.document_types().is_empty() { + let doc_types_key = format!("{}_doc_types", contract_id); + let doc_types_expanded = chooser_state.expanded_sections + .get(&contract_id) + .map(|s| s.contains(&doc_types_key)) + .unwrap_or(false); + + if render_collapsing_header(ui, "Document Types", doc_types_expanded, false, 1) { + let sections = chooser_state.expanded_sections + .entry(contract_id.clone()) + .or_default(); + if doc_types_expanded { + sections.remove(&doc_types_key); + } else { + sections.insert(doc_types_key.clone()); } - }); - - // Document Type clicked - if doc_resp.header_response.clicked() - && doc_resp.body_response.is_some() - { - // Expand doc type - if let Ok(new_doc_type) = contract - .contract - .document_type_cloned_for_name( - doc_name, - ) - { - *pending_document_type = - new_doc_type.clone(); - *selected_document_type = - new_doc_type.clone(); - *selected_data_contract = - contract.clone(); + } + + if doc_types_expanded { + ui.vertical(|ui| { + for (doc_name, doc_type) in contract.contract.document_types() { + let is_selected_doc_type = *selected_document_type == *doc_type; + let doc_type_key = format!("{}_{}", contract_id, doc_name); + + let doc_expanded = chooser_state.expanded_doc_types + .get(&contract_id) + .map(|s| s.contains(&doc_type_key)) + .unwrap_or(false); + + if render_collapsing_header(ui, doc_name, doc_expanded, is_selected_doc_type, 2) { + let doc_types = chooser_state.expanded_doc_types + .entry(contract_id.clone()) + .or_default(); + if doc_expanded { + doc_types.remove(&doc_type_key); + // Document Type collapsed + *selected_index = None; + *document_query = format!("SELECT * FROM {}", selected_document_type.name()); + } else { + doc_types.insert(doc_type_key.clone()); + // Document Type expanded + if let Ok(new_doc_type) = contract.contract.document_type_cloned_for_name(doc_name) { + *pending_document_type = new_doc_type.clone(); + *selected_document_type = new_doc_type.clone(); + *selected_data_contract = contract.clone(); *selected_index = None; - *document_query = format!( - "SELECT * FROM {}", - selected_document_type - .name() - ); + *document_query = format!("SELECT * FROM {}", selected_document_type.name()); // Reinitialize field selection - pending_fields_selection - .clear(); + pending_fields_selection.clear(); // Mark doc-defined fields - for (field_name, _schema) in - new_doc_type - .properties() - .iter() - { - pending_fields_selection - .insert( - field_name.clone(), - true, - ); + for (field_name, _schema) in new_doc_type.properties().iter() { + pending_fields_selection.insert(field_name.clone(), true); } // Show "internal" fields as unchecked by default, // except for $ownerId and $id, which are checked - for dash_field in - DOCUMENT_PRIVATE_FIELDS - { - let checked = *dash_field - == "$ownerId" - || *dash_field == "$id"; - pending_fields_selection - .insert( - dash_field - .to_string(), - checked, - ); + for dash_field in DOCUMENT_PRIVATE_FIELDS { + let checked = *dash_field == "$ownerId" || *dash_field == "$id"; + pending_fields_selection.insert(dash_field.to_string(), checked); } } } - // Document Type collapsed - else if doc_resp - .header_response - .clicked() - && doc_resp.body_response.is_none() - { - *selected_index = None; - *document_query = format!( - "SELECT * FROM {}", - selected_document_type.name() - ); - } } - }); - // - // ===== Tokens Section ===== - // - ClickableCollapsingHeader::new("Tokens") - .id_salt(format!("contract_{}_tokens", contract_id)) - .show(ui, |ui| { - let tokens_map = contract.contract.tokens(); - if tokens_map.is_empty() { - ui.label( - "No tokens defined for this contract.", - ); - } else { - for (token_name, token) in tokens_map { - // Each token is its own collapsible - ClickableCollapsingHeader::new(token_name.to_string()) - .id_salt(format!("contract_{}_token_{}", contract_id, token_name)) - .show(ui, |ui| { - // Now you can display base supply, max supply, etc. - ui.label(format!( - "Base Supply: {}", - token.base_supply() - )); - if let Some(max_supply) = - token.max_supply() - { - ui.label(format!( - "Max Supply: {}", - max_supply - )); - } else { - ui.label( - "Max Supply: None", - ); + if doc_expanded { + ui.vertical(|ui| { + // Show the indexes + if doc_type.indexes().is_empty() { + ui.add_space(4.0); + ui.label("No indexes defined"); + } else { + for (index_name, index) in doc_type.indexes() { + let is_selected_index = *selected_index == Some(index.clone()); + let index_key = format!("{}_{}_{}", contract_id, doc_name, index_name); + + let index_expanded = chooser_state.expanded_indexes + .get(&contract_id) + .map(|s| s.contains(&index_key)) + .unwrap_or(false); + + let index_label = format!("Index: {}", index_name); + if render_collapsing_header(ui, &index_label, index_expanded, is_selected_index, 3) { + let indexes = chooser_state.expanded_indexes + .entry(contract_id.clone()) + .or_default(); + if index_expanded { + indexes.remove(&index_key); + // Index collapsed + *selected_index = None; + *document_query = format!("SELECT * FROM {}", selected_document_type.name()); + } else { + indexes.insert(index_key.clone()); + // Index expanded + *selected_index = Some(index.clone()); + if let Ok(new_doc_type) = contract.contract.document_type_cloned_for_name(doc_name) { + *selected_document_type = new_doc_type; + *selected_data_contract = contract.clone(); + + // Build the WHERE clause using all property names + let conditions: Vec = index + .property_names() + .iter() + .map(|property_name| { + format!("`{}` = '___'", property_name) + }) + .collect(); + + let where_clause = if conditions.is_empty() { + String::new() + } else { + format!(" WHERE {}", conditions.join(" AND ")) + }; + + *document_query = format!( + "SELECT * FROM {}{}", + selected_document_type.name(), + where_clause + ); + } + } } - // Add more details here + if index_expanded { + ui.vertical(|ui| { + ui.add_space(4.0); + for prop in &index.properties { + ui.horizontal(|ui| { + ui.add_space(64.0); + ui.label(format!("{:?}", prop)); + }); + } + }); + } + } + } + }); + } + } + }); + } + } + + // + // ===== Tokens Section ===== + // + // Only show Tokens section if there are tokens + let tokens_map = contract.contract.tokens(); + if !tokens_map.is_empty() { + let tokens_key = format!("{}_tokens", contract_id); + let tokens_expanded = chooser_state.expanded_sections + .get(&contract_id) + .map(|s| s.contains(&tokens_key)) + .unwrap_or(false); + + if render_collapsing_header(ui, "Tokens", tokens_expanded, false, 1) { + let sections = chooser_state.expanded_sections + .entry(contract_id.clone()) + .or_default(); + if tokens_expanded { + sections.remove(&tokens_key); + } else { + sections.insert(tokens_key.clone()); + } + } + + if tokens_expanded { + ui.vertical(|ui| { + for (token_name, token) in tokens_map { + let token_key = format!("{}_token_{}", contract_id, token_name); + let token_expanded = chooser_state.expanded_tokens + .get(&contract_id) + .map(|s| s.contains(&token_key)) + .unwrap_or(false); + + if render_collapsing_header(ui, token_name.to_string(), token_expanded, false, 2) { + let tokens = chooser_state.expanded_tokens + .entry(contract_id.clone()) + .or_default(); + if token_expanded { + tokens.remove(&token_key); + } else { + tokens.insert(token_key.clone()); + } + } + + if token_expanded { + ui.vertical(|ui| { + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.add_space(32.0); + ui.label(format!("Base Supply: {}", token.base_supply())); }); + ui.horizontal(|ui| { + ui.add_space(32.0); + if let Some(max_supply) = token.max_supply() { + ui.label(format!("Max Supply: {}", max_supply)); + } else { + ui.label("Max Supply: None"); + } + }); + }); } } }); + } + } - // - // ===== Entire Contract JSON ===== - // - ClickableCollapsingHeader::new("Contract JSON") - .id_salt(format!("contract_{}_json", contract_id)) - .show(ui, |ui| { - match contract - .contract - .to_json(app_context.platform_version()) - { - Ok(json_value) => { - let pretty_str = - serde_json::to_string_pretty( - &json_value, - ) - .unwrap_or_else(|_| { - "Error formatting JSON" - .to_string() - }); + // + // ===== Entire Contract JSON ===== + // + let json_key = format!("{}_json", contract_id); + let json_expanded = chooser_state.expanded_sections + .get(&contract_id) + .map(|s| s.contains(&json_key)) + .unwrap_or(false); + + if render_collapsing_header(ui, "Contract JSON", json_expanded, false, 1) { + let sections = chooser_state.expanded_sections + .entry(contract_id.clone()) + .or_default(); + if json_expanded { + sections.remove(&json_key); + } else { + sections.insert(json_key.clone()); + } + } + + if json_expanded { + ui.vertical(|ui| { + match contract.contract.to_json(app_context.platform_version()) { + Ok(json_value) => { + let pretty_str = serde_json::to_string_pretty(&json_value) + .unwrap_or_else(|_| "Error formatting JSON".to_string()); - ui.add_space(2.0); + ui.add_space(2.0); - // A resizable region that the user can drag to expand/shrink - egui::Resize::default() - .id_salt( - "json_resize_area_for_contract", - ) - .default_size([400.0, 400.0]) // initial w,h + // A resizable region that the user can drag to expand/shrink + egui::Resize::default() + .id_salt(format!("json_resize_{}", contract_id)) + .default_size([400.0, 400.0]) .show(ui, |ui| { egui::ScrollArea::vertical() .auto_shrink([false; 2]) .show(ui, |ui| { - ui.monospace( - pretty_str, - ); + ui.monospace(pretty_str); }); }); - ui.add_space(3.0); - } - Err(e) => { - ui.label(format!( - "Error converting contract to JSON: {e}" - )); - } + ui.add_space(3.0); } - }); + Err(e) => { + ui.label(format!("Error converting contract to JSON: {e}")); + } + } }); + } + }); + + // Check for right-click on the contract header + // TODO: Add right-click support to custom header if needed - // Check for right-click on the contract header - if collapsing_response - .header_response - .secondary_clicked() + // Right‐aligned Remove button + ui.horizontal(|ui| { + ui.add_space(8.0); + if contract.alias != Some("dpns".to_string()) + && contract.alias != Some("token_history".to_string()) + && contract.alias != Some("withdrawals".to_string()) + && contract.alias != Some("keyword_search".to_string()) + && ui.add( + egui::Button::new("Remove") + .min_size(egui::Vec2::new(60.0, 20.0)) + .small() + ).clicked() { - let contract_id = contract - .contract - .id() - .to_string(Encoding::Base58); - chooser_state.right_click_contract_id = - Some(contract_id); - chooser_state.show_context_menu = true; - chooser_state.context_menu_position = ui - .ctx() - .pointer_interact_pos() - .unwrap_or(egui::Pos2::ZERO); + action |= AppAction::BackendTask( + BackendTask::ContractTask(Box::new( + ContractTask::RemoveContract(contract.contract.id()), + )), + ); } - - // Right‐aligned Remove button - ui.with_layout( - egui::Layout::right_to_left(egui::Align::Center), - |ui| { - ui.add_space(2.0); // Push down a few pixels - if contract.alias != Some("dpns".to_string()) - && contract.alias - != Some("token_history".to_string()) - && contract.alias - != Some("withdrawals".to_string()) - && contract.alias - != Some("keyword_search".to_string()) - && ui - .add( - egui::Button::new("X") - .min_size(egui::Vec2::new( - 20.0, 20.0, - )) - .small(), - ) - .clicked() - { - action |= AppAction::BackendTask( - BackendTask::ContractTask(Box::new( - ContractTask::RemoveContract( - contract.contract.id(), - ), - )), - ); - } - }, - ); }); - }, - ); + }); + } } }); }); @@ -489,63 +540,62 @@ pub fn add_contract_chooser_panel( }); // Show context menu if right-clicked - if chooser_state.show_context_menu { - if let Some(ref contract_id_str) = chooser_state.right_click_contract_id { - // Find the contract that was right-clicked - let contract_opt = contracts - .iter() - .find(|c| c.contract.id().to_string(Encoding::Base58) == *contract_id_str); - - if let Some(contract) = contract_opt { - egui::Window::new("Contract Menu") - .id(egui::Id::new("contract_context_menu")) - .title_bar(false) - .resizable(false) - .collapsible(false) - .fixed_pos(chooser_state.context_menu_position) - .show(ctx, |ui| { - ui.set_min_width(150.0); - - // Copy Hex option - if ui.button("Copy (Hex)").clicked() { - // Serialize contract to bytes - if let Ok(bytes) = - contract.contract.serialize_to_bytes_with_platform_version( - app_context.platform_version(), - ) - { - let hex_string = hex::encode(&bytes); - ui.ctx().copy_text(hex_string); - } - chooser_state.show_context_menu = false; + if chooser_state.show_context_menu + && let Some(ref contract_id_str) = chooser_state.right_click_contract_id + { + // Find the contract that was right-clicked + let contract_opt = contracts + .iter() + .find(|c| c.contract.id().to_string(Encoding::Base58) == *contract_id_str); + + if let Some(contract) = contract_opt { + egui::Window::new("Contract Menu") + .id(egui::Id::new("contract_context_menu")) + .title_bar(false) + .resizable(false) + .collapsible(false) + .fixed_pos(chooser_state.context_menu_position) + .show(ctx, |ui| { + ui.set_min_width(150.0); + + // Copy Hex option + if ui.button("Copy (Hex)").clicked() { + // Serialize contract to bytes + if let Ok(bytes) = + contract.contract.serialize_to_bytes_with_platform_version( + app_context.platform_version(), + ) + { + let hex_string = hex::encode(&bytes); + ui.ctx().copy_text(hex_string); } + chooser_state.show_context_menu = false; + } - // Copy JSON option - if ui.button("Copy (JSON)").clicked() { - // Convert contract to JSON - if let Ok(json_value) = - contract.contract.to_json(app_context.platform_version()) - { - if let Ok(json_string) = serde_json::to_string_pretty(&json_value) { - ui.ctx().copy_text(json_string); - } - } - chooser_state.show_context_menu = false; - } - }); - - // Close menu if clicked elsewhere - if ctx.input(|i| i.pointer.any_click()) { - // Check if click was outside the menu - let menu_rect = egui::Rect::from_min_size( - chooser_state.context_menu_position, - egui::vec2(150.0, 70.0), // Approximate size - ); - if let Some(pointer_pos) = ctx.pointer_interact_pos() { - if !menu_rect.contains(pointer_pos) { - chooser_state.show_context_menu = false; + // Copy JSON option + if ui.button("Copy (JSON)").clicked() { + // Convert contract to JSON + if let Ok(json_value) = + contract.contract.to_json(app_context.platform_version()) + && let Ok(json_string) = serde_json::to_string_pretty(&json_value) + { + ui.ctx().copy_text(json_string); } + chooser_state.show_context_menu = false; } + }); + + // Close menu if clicked elsewhere + if ctx.input(|i| i.pointer.any_click()) { + // Check if click was outside the menu + let menu_rect = egui::Rect::from_min_size( + chooser_state.context_menu_position, + egui::vec2(150.0, 70.0), // Approximate size + ); + if let Some(pointer_pos) = ctx.pointer_interact_pos() + && !menu_rect.contains(pointer_pos) + { + chooser_state.show_context_menu = false; } } } diff --git a/src/ui/components/contracts_subscreen_chooser_panel.rs b/src/ui/components/contracts_subscreen_chooser_panel.rs new file mode 100644 index 000000000..8b6768932 --- /dev/null +++ b/src/ui/components/contracts_subscreen_chooser_panel.rs @@ -0,0 +1,128 @@ +use crate::app::AppAction; +use crate::context::AppContext; +use crate::ui::theme::{DashColors, Shadow, Shape, Spacing, Typography}; +use crate::ui::{self, RootScreenType}; +use egui::{Context, Frame, Margin, RichText, SidePanel}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContractsSubscreen { + Contracts, + DPNS, + Dashpay, +} + +impl ContractsSubscreen { + pub fn display_name(&self) -> &'static str { + match self { + ContractsSubscreen::Contracts => "All Contracts", + ContractsSubscreen::DPNS => "DPNS", + ContractsSubscreen::Dashpay => "Dashpay", + } + } +} + +pub fn add_contracts_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext) -> AppAction { + let mut action = AppAction::None; + + let subscreens = vec![ + ContractsSubscreen::Contracts, + ContractsSubscreen::DPNS, + ContractsSubscreen::Dashpay, + ]; + + // Determine active selection from settings; default to Contracts + let active_screen = match app_context.get_settings() { + Ok(Some(settings)) => match settings.root_screen_type { + ui::RootScreenType::RootScreenDocumentQuery => ContractsSubscreen::Contracts, + ui::RootScreenType::RootScreenDPNSActiveContests + | ui::RootScreenType::RootScreenDPNSPastContests + | ui::RootScreenType::RootScreenDPNSOwnedNames + | ui::RootScreenType::RootScreenDPNSScheduledVotes => ContractsSubscreen::DPNS, + ui::RootScreenType::RootScreenDashpay => ContractsSubscreen::Dashpay, + _ => ContractsSubscreen::Contracts, + }, + _ => ContractsSubscreen::Contracts, + }; + + let dark_mode = ctx.style().visuals.dark_mode; + + SidePanel::left("contracts_subscreen_chooser_panel") + .resizable(false) + .default_width(270.0) + .frame( + Frame::new() + .fill(DashColors::background(dark_mode)) + .inner_margin(Margin::symmetric(10, 10)), + ) + .show(ctx, |ui| { + let available_height = ui.available_height(); + + Frame::new() + .fill(DashColors::surface(dark_mode)) + .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) + .inner_margin(Margin::same(Spacing::XL as i8)) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_LG)) + .shadow(Shadow::elevated()) + .show(ui, |ui| { + ui.set_min_height(available_height - 2.0 - (Spacing::XL * 2.0)); + ui.vertical(|ui| { + ui.label( + RichText::new("Contracts") + .font(Typography::heading_small()) + .color(DashColors::text_primary(dark_mode)), + ); + ui.add_space(Spacing::MD); + + for subscreen in subscreens { + let is_active = active_screen == subscreen; + + let button = if is_active { + egui::Button::new( + RichText::new(subscreen.display_name()) + .color(DashColors::WHITE) + .size(Typography::SCALE_SM), + ) + .fill(DashColors::DASH_BLUE) + .stroke(egui::Stroke::NONE) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .min_size(egui::Vec2::new(150.0, 28.0)) + } else { + egui::Button::new( + RichText::new(subscreen.display_name()) + .color(DashColors::text_primary(dark_mode)) + .size(Typography::SCALE_SM), + ) + .fill(DashColors::glass_white(dark_mode)) + .stroke(egui::Stroke::new(1.0, DashColors::border(dark_mode))) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .min_size(egui::Vec2::new(150.0, 28.0)) + }; + + if ui.add(button).clicked() { + action = match subscreen { + ContractsSubscreen::Contracts => { + AppAction::SetMainScreenThenGoToMainScreen( + RootScreenType::RootScreenDocumentQuery, + ) + } + ContractsSubscreen::DPNS => { + AppAction::SetMainScreenThenGoToMainScreen( + RootScreenType::RootScreenDPNSActiveContests, + ) + } + ContractsSubscreen::Dashpay => { + AppAction::SetMainScreenThenGoToMainScreen( + RootScreenType::RootScreenDashpay, + ) + } + }; + } + + ui.add_space(Spacing::SM); + } + }); + }); + }); + + action +} diff --git a/src/ui/components/dpns_subscreen_chooser_panel.rs b/src/ui/components/dpns_subscreen_chooser_panel.rs index 322ed63d8..34210c79d 100644 --- a/src/ui/components/dpns_subscreen_chooser_panel.rs +++ b/src/ui/components/dpns_subscreen_chooser_panel.rs @@ -17,38 +17,35 @@ pub fn add_dpns_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext) ]; let active_screen = match app_context.get_settings() { - Ok(Some(settings)) => match settings.1 { + Ok(Some(settings)) => match settings.root_screen_type { ui::RootScreenType::RootScreenDPNSActiveContests => DPNSSubscreen::Active, ui::RootScreenType::RootScreenDPNSPastContests => DPNSSubscreen::Past, ui::RootScreenType::RootScreenDPNSOwnedNames => DPNSSubscreen::Owned, ui::RootScreenType::RootScreenDPNSScheduledVotes => DPNSSubscreen::ScheduledVotes, _ => DPNSSubscreen::Active, }, - _ => DPNSSubscreen::Active, // Fallback to Active screen if settings unavailable + _ => DPNSSubscreen::Active, }; SidePanel::left("dpns_subscreen_chooser_panel") - .default_width(270.0) // Increased to account for margins + .resizable(true) + .default_width(270.0) .frame( Frame::new() - .fill(DashColors::background(dark_mode)) // Light background instead of transparent - .inner_margin(Margin::symmetric(10, 10)), // Add margins for island effect + .fill(DashColors::background(dark_mode)) + .inner_margin(Margin::symmetric(10, 10)), ) .show(ctx, |ui| { - // Fill the entire available height let available_height = ui.available_height(); - // Create an island panel with rounded edges that fills the height Frame::new() .fill(DashColors::surface(dark_mode)) .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) - .inner_margin(Margin::same(Spacing::MD_I8)) + .inner_margin(Margin::same(Spacing::XL as i8)) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_LG)) .shadow(Shadow::elevated()) .show(ui, |ui| { - // Account for both outer margin (10px * 2) and inner margin - ui.set_min_height(available_height - 2.0 - (Spacing::MD_I8 as f32 * 2.0)); - // Display subscreen names + ui.set_min_height(available_height - 2.0 - (Spacing::XL * 2.0)); ui.vertical(|ui| { ui.label( RichText::new("DPNS Subscreens") diff --git a/src/ui/components/identity_selector.rs b/src/ui/components/identity_selector.rs new file mode 100644 index 000000000..a30227dd3 --- /dev/null +++ b/src/ui/components/identity_selector.rs @@ -0,0 +1,278 @@ +use std::collections::BTreeMap; + +use crate::model::qualified_identity::QualifiedIdentity; +use dash_sdk::dpp::{ + identity::accessors::IdentityGettersV0, platform_value::string_encoding::Encoding, +}; +use dash_sdk::platform::Identifier; +use egui::{ComboBox, Response, TextEdit, Ui, Widget, WidgetText}; + +/// A reusable identity selector widget that combines a ComboBox dropdown of available identities +/// with a text edit field for manual entry. Implements the egui `Widget` trait for idiomatic usage. +/// +/// The widget includes an "Other" option in the dropdown that allows users to manually enter +/// identity addresses. When a known identity ID is entered in the text field, the corresponding +/// identity is automatically selected in the dropdown. +/// +/// # Example +/// ```rust +/// use dash_evo_tool::ui::components::identity_selector::IdentitySelector; +/// use dash_sdk::query_types::IndexMap; +/// use dash_sdk::platform::Identifier; +/// use dash_evo_tool::model::qualified_identity::QualifiedIdentity; +/// use egui::{RichText, Color32}; +/// +/// // This example shows the API usage, but cannot be run in doctest +/// // due to complex dependencies +/// fn example_usage(ui: &mut egui::Ui, identities: &[QualifiedIdentity]) { +/// let mut identity_str = String::new(); +/// let exclude_list = vec![/* some identifiers */]; +/// +/// // Basic usage with string label +/// let response1 = ui.add(IdentitySelector::new( +/// "my_selector1", +/// &mut identity_str, +/// identities +/// ) +/// .width(250.0) +/// .label("Select Identity:") // accepts &str +/// .exclude(&exclude_list)); +/// +/// // Advanced usage with styled RichText label +/// let response2 = ui.add(IdentitySelector::new( +/// "my_selector2", +/// &mut identity_str, +/// identities +/// ) +/// .width(300.0) +/// .label(RichText::new("Styled Label").color(Color32::RED).strong()) // accepts RichText +/// .exclude(&exclude_list)); +/// +/// if response1.changed() || response2.changed() { +/// // Identity was changed via dropdown selection, "Other" selection, or text input +/// } +/// } +/// ``` +pub struct IdentitySelector<'a> { + /// A unique ID for this selector (used for egui's ID system) + id: String, + /// Width of the ComboBox + width: f32, + /// Mutable reference to the current identity string + identity_str: &'a mut String, + /// Selected identity, if any + identity: Option<&'a mut Option>, + /// Map of available identities to choose from + identities: BTreeMap, + /// Slice of identity strings to exclude from dropdown (can be empty) + exclude_identities: &'a [Identifier], + /// Optional label to display before the selector + label: Option, + other_option: bool, +} + +impl<'a> IdentitySelector<'a> { + /// Create a new IdentitySelector with the given ID and required parameters + pub fn new>( + id: impl Into, + identity_str: &'a mut String, + identities: &'a [I], + ) -> Self { + Self { + id: id.into(), + width: 200.0, + identity_str, + identity: None, + identities: identities + .iter() + .map(|q| { + let id = q.as_ref(); + (id.identity.id(), id) + }) + .collect(), + exclude_identities: &[], + label: None, + other_option: true, // Default to showing "Other" option + } + } + + /// This method creates a selector that can update a mutable reference to the selected identity + /// based on user input. This is useful when you want to allow users to select from existing identities + /// or enter a new one, while keeping track of the selected identity in a mutable reference. + /// + /// `selected_identity` will be set to: + /// * `Some(qualified_identity)` if a known identity is selected from the dropdown + /// * `None` if the "Other" option is selected or the text input is empty or invalid + pub fn selected_identity( + mut self, + selected_identity: &'a mut Option, + ) -> Result { + self.identity = Some(selected_identity); + // trigger change handling to initialize the state + + Ok(self) + } + + /// Enable or disable the "Other" option in the dropdown + pub fn other_option(mut self, other_option: bool) -> Self { + self.other_option = other_option; + + self + } + + /// Set the width of the ComboBox + pub fn width(mut self, width: f32) -> Self { + self.width = width; + self + } + + /// Set the identities to exclude from the dropdown + pub fn exclude(mut self, exclude_identities: &'a [Identifier]) -> Self { + self.exclude_identities = exclude_identities; + self + } + + /// Set an optional label to display before the selector + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + + /// Validate the given identity string and return the corresponding identity if valid. + fn get_identity(&self, identity_str: &str) -> Option<&'a QualifiedIdentity> { + let identifier = Identifier::from_string_unknown_encoding(identity_str).ok()?; + + if self.exclude_identities.contains(&identifier) { + return None; + } + + self.identities.get(&identifier).copied() + } + + /// Handle changes to the identity selector + fn on_change(&mut self) { + let selected_identity = self.get_identity(self.identity_str); + if let Some(self_identity) = &mut self.identity { + if let Some(new_identity) = selected_identity { + self_identity.replace(new_identity.clone()); + tracing::trace!( + "updating selected identity: {:?} {:?}", + new_identity, + self.identity, + ); + } else { + self_identity.take(); // Clear the existing identity reference if it was None + }; + } + } +} + +impl<'a> Widget for IdentitySelector<'a> { + /// Render the identity selector widget + /// + /// ## Panics + /// + /// This method will panic if there are no identities available to select from + /// and no "Other" option is enabled. It requires at least one identity to function + /// correctly, as it needs to provide a default selection. + fn ui(mut self, ui: &mut Ui) -> Response { + ui.horizontal(|ui| { + // Display label if present, with centered vertical alignment + if let Some(label) = &self.label { + ui.vertical(|ui| { + // FIXME we add space because vertical alignment is not working as expected + ui.add_space(15.0); + ui.add(egui::Label::new(label.clone())); + }); + } + + // If the "Other" option is disabled, we automatically select first identity + if !self.other_option + && self.identity_str.is_empty() + && let Some(first_identity) = self + .identities + .keys() + .find(|id| !self.exclude_identities.contains(id)) + { + *self.identity_str = first_identity.to_string(Encoding::Base58); + // trigger change handling to update the selected identity + self.on_change(); + } + + // Check if current identity_str matches any existing identity; current_identity = None means + // no identity is selected or the input is empty. + let current_identity = self.get_identity(self.identity_str); + + let has_matching_identity = current_identity.is_some(); + + let current_identity_combo_label = current_identity + .map(|q| q.display_string()) + .unwrap_or_else(|| { + if self.other_option { + "Other".to_string() + } else { + "No identities found".to_string() + } + }); + + // ComboBox for selecting existing identities + let combo_response = ComboBox::from_id_salt(&self.id) + .width(self.width) + .selected_text(current_identity_combo_label) + .show_ui(ui, |ui| { + let mut combo_changed = false; + + // Add existing identities to the dropdown + for (identifier, qualified_identity) in self.identities.iter() { + // Filter out excluded identities + if self.exclude_identities.contains(identifier) { + continue; + } + let id_str = identifier.to_string(Encoding::Base58); + let checked = current_identity.is_some_and(|x| qualified_identity.eq(&x)); + + if ui + .selectable_label(checked, qualified_identity.display_string()) + .clicked() + { + combo_changed = true; + *self.identity_str = id_str; + } + } + + // Add "Other" option + if self.other_option + && ui + .selectable_label(!has_matching_identity, "Other") + .clicked() + { + self.identity_str.clear(); + combo_changed = true; + } + + combo_changed + }); + + // Text edit field for manual entry + let text_response = TextEdit::singleline(self.identity_str) + .interactive(self.other_option) + .ui(ui); + + // Handle identity selection updates after combo box and text input + let combo_changed = combo_response.inner.unwrap_or(false); + if combo_changed || text_response.changed() { + self.on_change(); + } + + // Return a response that indicates if anything changed + let mut response = text_response; + if combo_changed { + // note: response inherits the changed state from text_response + response.mark_changed(); + } + + response + }) + .inner + } +} diff --git a/src/ui/components/left_panel.rs b/src/ui/components/left_panel.rs index 6b34a0f2c..7e084c0a4 100644 --- a/src/ui/components/left_panel.rs +++ b/src/ui/components/left_panel.rs @@ -6,6 +6,7 @@ use crate::ui::theme::{DashColors, Shadow, Shape, Spacing}; use dash_sdk::dashcore_rpc::dashcore::Network; use eframe::epaint::Margin; use egui::{Color32, Context, Frame, ImageButton, RichText, SidePanel, TextureHandle}; +use egui_extras::{Size, StripBuilder}; use rust_embed::RustEmbed; use std::sync::Arc; @@ -56,21 +57,36 @@ pub fn add_left_panel( // Define the button details directly in this function let buttons = [ - ("I", RootScreenType::RootScreenIdentities, "identity.png"), - ("Q", RootScreenType::RootScreenDocumentQuery, "doc.png"), - ("O", RootScreenType::RootScreenMyTokenBalances, "tokens.png"), ( - "C", - RootScreenType::RootScreenDPNSActiveContests, - "voting.png", + "Identities", + RootScreenType::RootScreenIdentities, + "identity.png", ), - ("W", RootScreenType::RootScreenWalletsBalances, "wallet.png"), ( - "T", - RootScreenType::RootScreenToolsProofLogScreen, + "Contracts", + RootScreenType::RootScreenDocumentQuery, + "doc.png", + ), + ( + "Tokens", + RootScreenType::RootScreenMyTokenBalances, + "tokens.png", + ), + ( + "Wallets", + RootScreenType::RootScreenWalletsBalances, + "wallet.png", + ), + ( + "Tools", + RootScreenType::RootScreenToolsPlatformInfoScreen, "tools.png", ), - ("N", RootScreenType::RootScreenNetworkChooser, "config.png"), + ( + "Settings", + RootScreenType::RootScreenNetworkChooser, + "config.png", + ), ]; let panel_width = 60.0 + (Spacing::MD * 2.0); // Button width + margins @@ -79,6 +95,7 @@ pub fn add_left_panel( SidePanel::left("left_panel") .default_width(panel_width + 20.0) // Add extra width for margins + .resizable(false) .frame( Frame::new() .fill(DashColors::background(dark_mode)) @@ -93,119 +110,170 @@ pub fn add_left_panel( .corner_radius(egui::CornerRadius::same(Shape::RADIUS_LG)) .shadow(Shadow::elevated()) .show(ui, |ui| { - ui.vertical_centered(|ui| { - for (label, screen_type, icon_path) in buttons.iter() { - let texture: Option = load_icon(ctx, icon_path); - let is_selected = selected_screen == *screen_type; - - let button_color = if is_selected { - Color32::WHITE // Bright white for selected - } else if dark_mode { - Color32::from_rgb(180, 180, 180) // Bright gray for visibility in dark mode - } else { - Color32::from_rgb(160, 160, 160) // Medium gray for contrast in light mode - }; - - // Add icon-based button if texture is loaded - if let Some(ref texture) = texture { - let button = - ImageButton::new(texture).frame(false).tint(button_color); - - let added = ui.add(button); - if added.clicked() { - action = - AppAction::SetMainScreenThenGoToMainScreen(*screen_type); - } else if added.hovered() { - ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); - } - } else { - // Fallback to a modern gradient button if texture loading fails - if is_selected { - if GradientButton::new(*label, app_context) - .min_width(60.0) - .glow() - .show(ui) - .clicked() - { - action = AppAction::SetMainScreen(*screen_type); - } - } else { - let button = egui::Button::new(*label) - .fill(DashColors::glass_white(dark_mode)) - .stroke(egui::Stroke::new( - 1.0, - DashColors::glass_border(dark_mode), - )) - .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) - .min_size(egui::vec2(60.0, 60.0)); - - if ui.add(button).clicked() { - action = AppAction::SetMainScreen(*screen_type); - } - } - } - - ui.add_space(Spacing::MD); // Add some space between buttons - } - - // Push content to the top and dev label + logo to the bottom - ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { - if app_context.is_developer_mode() { - ui.add_space(Spacing::MD); - let dev_label = egui::RichText::new("🔧 Dev mode") - .color(DashColors::GRADIENT_PURPLE) - .size(12.0); - if ui.label(dev_label).clicked() { - action = AppAction::SetMainScreenThenGoToMainScreen( - RootScreenType::RootScreenNetworkChooser, - ); - }; - } - - // Show network name if not on main Dash network - if app_context.network != Network::Dash { - let (network_name, network_color) = match app_context.network { - Network::Testnet => ("Testnet", Color32::from_rgb(255, 165, 0)), - Network::Devnet => ("Devnet", Color32::DARK_RED), - Network::Regtest => { - ("Local Network", Color32::from_rgb(139, 69, 19)) - } - _ => ("Unknown", DashColors::DASH_BLUE), - }; - - ui.label( - RichText::new(network_name) - .color(network_color) - .size(12.0) - .strong(), - ); - ui.add_space(2.0); - } - - // Add Dash logo at the bottom - if let Some(dash_texture) = load_icon(ctx, "dash.png") { - if app_context.network == Network::Dash { - ui.add_space(Spacing::SM); - } - let logo_size = egui::vec2(50.0, 20.0); // Even smaller size, same aspect ratio - let logo_response = ui.add( - egui::Image::new(&dash_texture) - .fit_to_exact_size(logo_size) - .texture_options(egui::TextureOptions::LINEAR) // Smooth interpolation to reduce pixelation - .sense(egui::Sense::click()), - ); + // Reserve a fixed area at the bottom for the logo and labels, + // and make the button list above it vertically scrollable. + let mut bottom_reserved = Spacing::SM + 20.0; // spacing + logo height + if app_context.network != Network::Dash { + bottom_reserved += 22.0; // network label + spacing + } + if app_context.is_developer_mode() { + bottom_reserved += Spacing::MD + 16.0; // dev label area + } + + StripBuilder::new(ui) + .size(Size::remainder()) // top: fills remaining height + .size(Size::exact(bottom_reserved.max(40.0))) // bottom: reserved area + .vertical(|mut strip| { + // Top cell: scrollable list of buttons + strip.cell(|ui| { + egui::ScrollArea::vertical() + .id_salt("left_panel_buttons_scroll") + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.vertical_centered(|ui| { + for (label, screen_type, icon_path) in buttons.iter() { + let texture: Option = load_icon(ctx, icon_path); + let is_selected = selected_screen == *screen_type; - if logo_response.clicked() { - ui.ctx() - .open_url(egui::OpenUrl::new_tab("https://dash.org")); - } + let button_color = if is_selected { + Color32::WHITE + } else if dark_mode { + Color32::from_rgb(180, 180, 180) + } else { + Color32::from_rgb(160, 160, 160) + }; - if logo_response.hovered() { - ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand); - } - } + if let Some(ref texture) = texture { + let button = ImageButton::new(texture) + .frame(false) + .tint(button_color); + + let added = ui.add(button); + if added.clicked() { + action = AppAction::SetMainScreenThenGoToMainScreen( + *screen_type, + ); + } else if added.hovered() { + ui.ctx().set_cursor_icon( + egui::CursorIcon::PointingHand, + ); + } + // Put the label beneath the icon + let color = if is_selected { + DashColors::DASH_BLUE + } else { + DashColors::text_primary(dark_mode) + }; + let label_text = + RichText::new(*label).color(color).size(13.0); + ui.label(label_text); + } else { + // Fallback button if texture not available + if is_selected { + if GradientButton::new(*label, app_context) + .min_width(60.0) + .glow() + .show(ui) + .clicked() + { + action = AppAction::SetMainScreen(*screen_type); + } + } else { + let button = egui::Button::new(*label) + .fill(DashColors::glass_white(dark_mode)) + .stroke(egui::Stroke::new( + 1.0, + DashColors::glass_border(dark_mode), + )) + .corner_radius(egui::CornerRadius::same( + Shape::RADIUS_MD, + )) + .min_size(egui::vec2(60.0, 60.0)); + + if ui.add(button).clicked() { + action = AppAction::SetMainScreen(*screen_type); + } + } + } + + ui.add_space(Spacing::MD); + } + }); + }); + }); + + // Bottom cell: always visible logo and labels + strip.cell(|ui| { + ui.with_layout( + egui::Layout::bottom_up(egui::Align::Center), + |ui| { + // Dash logo at the very bottom + if let Some(dash_texture) = load_icon(ctx, "dash.png") { + if app_context.network == Network::Dash { + ui.add_space(Spacing::SM); + } + let logo_size = egui::vec2(50.0, 20.0); + let logo_response = ui.add( + egui::Image::new(&dash_texture) + .fit_to_exact_size(logo_size) + .texture_options(egui::TextureOptions::LINEAR) + .sense(egui::Sense::click()), + ); + + if logo_response.clicked() { + ui.ctx() + .open_url(egui::OpenUrl::new_tab("https://dash.org")); + } + + if logo_response.hovered() { + ui.ctx() + .set_cursor_icon(egui::CursorIcon::PointingHand); + } + } + + // Network label (if not on mainnet) + if app_context.network != Network::Dash { + let (network_name, network_color) = match app_context.network { + Network::Testnet => ( + "Testnet", + Color32::from_rgb(255, 165, 0), + ), + Network::Devnet => ( + "Devnet", + Color32::DARK_RED, + ), + Network::Regtest => ( + "Local Network", + Color32::from_rgb(139, 69, 19), + ), + _ => ("Unknown", DashColors::DASH_BLUE), + }; + + ui.add_space(2.0); + ui.label( + RichText::new(network_name) + .color(network_color) + .size(12.0) + .strong(), + ); + } + + // Dev mode label (above network label if present) + if app_context.is_developer_mode() { + ui.add_space(Spacing::MD); + let dev_label = egui::RichText::new("🔧 Dev mode") + .color(DashColors::GRADIENT_PURPLE) + .size(12.0); + if ui.label(dev_label).clicked() { + action = AppAction::SetMainScreenThenGoToMainScreen( + RootScreenType::RootScreenNetworkChooser, + ); + } + } + }, + ); + }); }); - }); }); // Close the island frame }); diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index 366f2e2bc..53c6e06ab 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,7 +1,12 @@ +pub mod amount_input; pub mod clickable_collapsing_header; +pub mod component_trait; +pub mod confirmation_dialog; pub mod contract_chooser_panel; +pub mod contracts_subscreen_chooser_panel; pub mod dpns_subscreen_chooser_panel; pub mod entropy_grid; +pub mod identity_selector; pub mod left_panel; pub mod left_wallet_panel; pub mod styled; @@ -9,3 +14,6 @@ pub mod tokens_subscreen_chooser_panel; pub mod tools_subscreen_chooser_panel; pub mod top_panel; pub mod wallet_unlock; + +// Re-export the main traits for easy access +pub use component_trait::{Component, ComponentResponse}; diff --git a/src/ui/components/styled.rs b/src/ui/components/styled.rs index 98972e7a6..448b9e4e7 100644 --- a/src/ui/components/styled.rs +++ b/src/ui/components/styled.rs @@ -11,6 +11,7 @@ use egui::{ // Re-export commonly used components pub use super::clickable_collapsing_header::ClickableCollapsingHeader; +pub use super::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; /// Styled button variants #[allow(dead_code)] diff --git a/src/ui/components/tokens_subscreen_chooser_panel.rs b/src/ui/components/tokens_subscreen_chooser_panel.rs index d5a38a519..add77bd30 100644 --- a/src/ui/components/tokens_subscreen_chooser_panel.rs +++ b/src/ui/components/tokens_subscreen_chooser_panel.rs @@ -15,7 +15,7 @@ pub fn add_tokens_subscreen_chooser_panel(ctx: &Context, app_context: &AppContex ]; let active_screen = match app_context.get_settings() { - Ok(Some(settings)) => match settings.1 { + Ok(Some(settings)) => match settings.root_screen_type { ui::RootScreenType::RootScreenMyTokenBalances => TokensSubscreen::MyTokens, ui::RootScreenType::RootScreenTokenSearch => TokensSubscreen::SearchTokens, ui::RootScreenType::RootScreenTokenCreator => TokensSubscreen::TokenCreator, @@ -27,18 +27,15 @@ pub fn add_tokens_subscreen_chooser_panel(ctx: &Context, app_context: &AppContex let dark_mode = ctx.style().visuals.dark_mode; SidePanel::left("tokens_subscreen_chooser_panel") - .resizable(true) - .default_width(270.0) // Increased to account for margins + .resizable(false) + .default_width(270.0) .frame( Frame::new() .fill(DashColors::background(dark_mode)) - .inner_margin(Margin::symmetric(10, 10)), // Add margins for island effect + .inner_margin(Margin::symmetric(10, 10)), ) .show(ctx, |ui| { - // Fill the entire available height let available_height = ui.available_height(); - - // Create an island panel with rounded edges that fills the height Frame::new() .fill(DashColors::surface(dark_mode)) .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) @@ -46,7 +43,6 @@ pub fn add_tokens_subscreen_chooser_panel(ctx: &Context, app_context: &AppContex .corner_radius(egui::CornerRadius::same(Shape::RADIUS_LG)) .shadow(Shadow::elevated()) .show(ui, |ui| { - // Account for both outer margin (10px * 2) and inner margin ui.set_min_height(available_height - 2.0 - (Spacing::XL * 2.0)); // Display subscreen names ui.vertical(|ui| { diff --git a/src/ui/components/tools_subscreen_chooser_panel.rs b/src/ui/components/tools_subscreen_chooser_panel.rs index 58e5d8e74..05a9db9b6 100644 --- a/src/ui/components/tools_subscreen_chooser_panel.rs +++ b/src/ui/components/tools_subscreen_chooser_panel.rs @@ -6,23 +6,27 @@ use egui::{Context, Frame, Margin, RichText, SidePanel}; #[derive(PartialEq)] pub enum ToolsSubscreen { + PlatformInfo, ProofLog, TransactionViewer, DocumentViewer, ProofViewer, ContractViewer, - PlatformInfo, + GroveSTARK, + MasternodeListDiff, } impl ToolsSubscreen { pub fn display_name(&self) -> &'static str { match self { + Self::PlatformInfo => "Platform info", Self::ProofLog => "Proof logs", Self::TransactionViewer => "Transaction deserializer", Self::ProofViewer => "Proof deserializer", Self::DocumentViewer => "Document deserializer", Self::ContractViewer => "Contract deserializer", - Self::PlatformInfo => "Platform info", + Self::GroveSTARK => "ZK Proofs", + Self::MasternodeListDiff => "Masternode list diff inspector", } } } @@ -32,16 +36,19 @@ pub fn add_tools_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext let dark_mode = ctx.style().visuals.dark_mode; let subscreens = vec![ + ToolsSubscreen::PlatformInfo, ToolsSubscreen::ProofLog, ToolsSubscreen::ProofViewer, ToolsSubscreen::TransactionViewer, ToolsSubscreen::DocumentViewer, ToolsSubscreen::ContractViewer, - ToolsSubscreen::PlatformInfo, + ToolsSubscreen::GroveSTARK, + ToolsSubscreen::MasternodeListDiff, ]; let active_screen = match app_context.get_settings() { - Ok(Some(settings)) => match settings.1 { + Ok(Some(settings)) => match settings.root_screen_type { + ui::RootScreenType::RootScreenToolsPlatformInfoScreen => ToolsSubscreen::PlatformInfo, ui::RootScreenType::RootScreenToolsProofLogScreen => ToolsSubscreen::ProofLog, ui::RootScreenType::RootScreenToolsTransitionVisualizerScreen => { ToolsSubscreen::TransactionViewer @@ -53,34 +60,33 @@ pub fn add_tools_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext ui::RootScreenType::RootScreenToolsContractVisualizerScreen => { ToolsSubscreen::ContractViewer } - ui::RootScreenType::RootScreenToolsPlatformInfoScreen => ToolsSubscreen::PlatformInfo, - _ => ToolsSubscreen::ProofLog, + ui::RootScreenType::RootScreenToolsMasternodeListDiffScreen => { + ToolsSubscreen::MasternodeListDiff + } + ui::RootScreenType::RootScreenToolsGroveSTARKScreen => ToolsSubscreen::GroveSTARK, + _ => ToolsSubscreen::PlatformInfo, }, - _ => ToolsSubscreen::ProofLog, // Fallback to Active screen if settings unavailable + _ => ToolsSubscreen::PlatformInfo, // Fallback to Active screen if settings unavailable }; SidePanel::left("tools_subscreen_chooser_panel") - .default_width(270.0) // Increased to account for margins + .resizable(false) + .default_width(270.0) .frame( Frame::new() - .fill(DashColors::background(dark_mode)) // Light background instead of transparent - .inner_margin(Margin::symmetric(10, 10)), // Add margins for island effect + .fill(DashColors::background(dark_mode)) + .inner_margin(Margin::symmetric(10, 10)), ) .show(ctx, |ui| { - // Fill the entire available height let available_height = ui.available_height(); - - // Create an island panel with rounded edges that fills the height Frame::new() .fill(DashColors::surface(dark_mode)) .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) - .inner_margin(Margin::same(Spacing::MD_I8)) + .inner_margin(Margin::same(Spacing::XL as i8)) .corner_radius(egui::CornerRadius::same(Shape::RADIUS_LG)) .shadow(Shadow::elevated()) .show(ui, |ui| { - // Account for both outer margin (10px * 2) and inner margin - ui.set_min_height(available_height - 2.0 - (Spacing::MD_I8 as f32 * 2.0)); - // Display subscreen names + ui.set_min_height(available_height - 2.0 - (Spacing::XL * 2.0)); ui.vertical(|ui| { ui.label( RichText::new("Tools") @@ -118,43 +124,50 @@ pub fn add_tools_subscreen_chooser_panel(ctx: &Context, app_context: &AppContext if ui.add(button).clicked() { // Handle navigation based on which subscreen is selected match subscreen { - ToolsSubscreen::ProofLog => { - action = AppAction::SetMainScreen( - RootScreenType::RootScreenToolsProofLogScreen, - ) - } - ToolsSubscreen::TransactionViewer => { - action = AppAction::SetMainScreen( - RootScreenType::RootScreenToolsTransitionVisualizerScreen, - ) - } - ToolsSubscreen::ProofViewer => { - action = AppAction::SetMainScreen( - RootScreenType::RootScreenToolsProofVisualizerScreen, - ) - } - ToolsSubscreen::DocumentViewer => { - action = AppAction::SetMainScreen( - RootScreenType::RootScreenToolsDocumentVisualizerScreen, - ) - } - ToolsSubscreen::ContractViewer => { - action = AppAction::SetMainScreen( - RootScreenType::RootScreenToolsContractVisualizerScreen, - ) - } - ToolsSubscreen::PlatformInfo => { - action = AppAction::SetMainScreen( - RootScreenType::RootScreenToolsPlatformInfoScreen, - ) + ToolsSubscreen::PlatformInfo => { + action = AppAction::SetMainScreen( + RootScreenType::RootScreenToolsPlatformInfoScreen, + ) + } + ToolsSubscreen::ProofLog => { + action = AppAction::SetMainScreen( + RootScreenType::RootScreenToolsProofLogScreen, + ) + } + ToolsSubscreen::TransactionViewer => { + action = AppAction::SetMainScreen( + RootScreenType::RootScreenToolsTransitionVisualizerScreen, + ) + } + ToolsSubscreen::ProofViewer => { + action = AppAction::SetMainScreen( + RootScreenType::RootScreenToolsProofVisualizerScreen, + ) + } + ToolsSubscreen::DocumentViewer => { + action = AppAction::SetMainScreen( + RootScreenType::RootScreenToolsDocumentVisualizerScreen, + ) + } + ToolsSubscreen::ContractViewer => { + action = AppAction::SetMainScreen( + RootScreenType::RootScreenToolsContractVisualizerScreen, + ) + } + ToolsSubscreen::MasternodeListDiff => { + action = AppAction::SetMainScreen( + RootScreenType::RootScreenToolsMasternodeListDiffScreen) + } + ToolsSubscreen::GroveSTARK => { + action = AppAction::SetMainScreen( + RootScreenType::RootScreenToolsGroveSTARKScreen) + } + } } - } - } - ui.add_space(Spacing::SM); } }); - }); // Close the island frame + }); }); action diff --git a/src/ui/components/top_panel.rs b/src/ui/components/top_panel.rs index 3c2368b99..84e606194 100644 --- a/src/ui/components/top_panel.rs +++ b/src/ui/components/top_panel.rs @@ -60,13 +60,13 @@ fn add_location_view(ui: &mut Ui, location: Vec<(&str, AppAction)>, dark_mode: b // Apply negative vertical offset to move text up let offset = egui::vec2(0.0, -7.0); - ui.allocate_new_ui( + ui.scope_builder( egui::UiBuilder::new().max_rect(egui::Rect::from_min_size( ui.cursor().min + offset, ui.available_size(), )), |ui| { - egui::menu::bar(ui, |ui| { + egui::MenuBar::new().ui(ui, |ui| { ui.horizontal(|ui| { let len = location.len(); for (idx, (text, loc_action)) in location.into_iter().enumerate() { @@ -122,7 +122,7 @@ fn add_connection_indicator(ui: &mut Ui, app_context: &Arc) -> AppAc // Wrap in a container that can be positioned vertically ui.allocate_ui(ui.available_size(), |ui| { - ui.allocate_new_ui( + ui.scope_builder( egui::UiBuilder::new().max_rect(egui::Rect::from_min_size( ui.cursor().min, ui.available_size(), @@ -159,9 +159,10 @@ fn add_connection_indicator(ui: &mut Ui, app_context: &Arc) -> AppAc let resp = resp.on_hover_text(tip); if resp.clicked() && !connected { - let settings = app_context.db.get_settings().ok().flatten(); + let settings = app_context.get_settings().ok().flatten(); + let (custom_path, overwrite) = settings - .map(|(_, _, _, custom_path, overwrite, _)| (custom_path, overwrite)) + .map(|s| (s.dash_qt_path, s.overwrite_dash_conf)) .unwrap_or((None, true)); if let Some(dash_qt_path) = custom_path { action |= AppAction::BackendTask(BackendTask::CoreTask( @@ -327,29 +328,28 @@ pub fn add_top_panel( .stroke(Stroke::NONE) .min_size(egui::vec2(100.0, 30.0)); - // a unique ID for the popup - let popup_id = ui.auto_id_with("documents_popup"); let resp = ui.add(docs_btn); - if resp.clicked() { - ui.memory_mut(|mem| mem.toggle_popup(popup_id)); - } + let popup_id = ui.make_persistent_id("docs_popup"); - // open the popup directly below the button - egui::popup::popup_below_widget( - ui, + egui::Popup::new( popup_id, + ui.ctx().clone(), &resp, - egui::popup::PopupCloseBehavior::CloseOnClickOutside, - |ui| { - ui.set_min_width(150.0); - for (text, da) in doc_actions { - if ui.button(text).clicked() { - action = da.create_action(app_context); - ui.close_menu(); - } + resp.layer_id, + ) + .open_memory( + resp.clicked().then_some(egui::SetOpenCommand::Toggle), + ) + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) + .show(|ui| { + ui.set_min_width(150.0); + for (text, da) in doc_actions { + if ui.button(text).clicked() { + action = da.create_action(app_context); + // ui.close(); } - }, - ); + } + }); } // Grouped Contracts menu @@ -367,25 +367,26 @@ pub fn add_top_panel( let popup_id = ui.auto_id_with("contracts_popup"); let resp = ui.add(contracts_btn); - if resp.clicked() { - ui.memory_mut(|mem| mem.toggle_popup(popup_id)); - } - egui::popup::popup_below_widget( - ui, + egui::Popup::new( popup_id, + ui.ctx().clone(), &resp, - egui::popup::PopupCloseBehavior::CloseOnClickOutside, - |ui| { - ui.set_min_width(150.0); - for (text, ca) in contract_actions { - if ui.button(text).clicked() { - action = ca.create_action(app_context); - ui.close_menu(); - } + resp.layer_id, + ) + .open_memory( + resp.clicked().then_some(egui::SetOpenCommand::Toggle), + ) + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) + .show(|ui| { + ui.set_min_width(150.0); + for (text, ca) in contract_actions { + if ui.button(text).clicked() { + action = ca.create_action(app_context); + ui.close(); } - }, - ); + } + }); } // Render other buttons normally diff --git a/src/ui/contracts_documents/add_contracts_screen.rs b/src/ui/contracts_documents/add_contracts_screen.rs index f8de7ab88..af2879347 100644 --- a/src/ui/contracts_documents/add_contracts_screen.rs +++ b/src/ui/contracts_documents/add_contracts_screen.rs @@ -323,6 +323,12 @@ impl ScreenLike for AddContractsScreen { crate::ui::RootScreenType::RootScreenDocumentQuery, ); + // Contracts sub-left panel + action |= crate::ui::components::contracts_subscreen_chooser_panel::add_contracts_subscreen_chooser_panel( + ctx, + &self.app_context, + ); + action |= island_central_panel(ctx, |ui| { ui.heading("Add Contracts"); ui.add_space(10.0); diff --git a/src/ui/contracts_documents/contracts_documents_screen.rs b/src/ui/contracts_documents/contracts_documents_screen.rs index 259cc50ef..f293b8621 100644 --- a/src/ui/contracts_documents/contracts_documents_screen.rs +++ b/src/ui/contracts_documents/contracts_documents_screen.rs @@ -4,6 +4,8 @@ use crate::backend_task::contract::ContractTask; use crate::backend_task::document::DocumentTask::{self, FetchDocumentsPage}; // Updated import use crate::context::AppContext; use crate::model::qualified_contract::QualifiedContract; +use crate::ui::components::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::contract_chooser_panel::{ ContractChooserState, add_contract_chooser_panel, }; @@ -57,7 +59,7 @@ pub struct DocumentQueryScreen { selected_index: Option, pub matching_documents: Vec, document_query_status: DocumentQueryStatus, - confirm_remove_contract_popup: bool, + confirmation_dialog: Option, contract_to_remove: Option, pending_document_type: DocumentType, pending_fields_selection: HashMap, @@ -122,7 +124,7 @@ impl DocumentQueryScreen { selected_index: None, matching_documents: vec![], document_query_status: DocumentQueryStatus::NotStarted, - confirm_remove_contract_popup: false, + confirmation_dialog: None, contract_to_remove: None, pending_document_type, pending_fields_selection, @@ -490,53 +492,44 @@ impl DocumentQueryScreen { let contract_to_remove = match &self.contract_to_remove { Some(contract) => *contract, None => { - self.confirm_remove_contract_popup = false; + self.confirmation_dialog = None; return AppAction::None; } }; - let mut app_action = AppAction::None; - let mut is_open = true; - - egui::Window::new("Confirm Remove Contract") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - let contract_alias_or_id = - match self.app_context.get_contract_by_id(&contract_to_remove) { - Ok(Some(contract)) => contract - .alias - .unwrap_or_else(|| contract.contract.id().to_string(Encoding::Base58)), - Ok(None) | Err(_) => contract_to_remove.to_string(Encoding::Base58), - }; - - ui.label(format!( + let contract_alias_or_id = match self.app_context.get_contract_by_id(&contract_to_remove) { + Ok(Some(contract)) => contract + .alias + .unwrap_or_else(|| contract.contract.id().to_string(Encoding::Base58)), + Ok(None) | Err(_) => contract_to_remove.to_string(Encoding::Base58), + }; + + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Remove Contract".to_string(), + format!( "Are you sure you want to remove contract \"{}\"?", contract_alias_or_id - )); - - // Confirm button - if ui.button("Confirm").clicked() { - app_action = AppAction::BackendTask(BackendTask::ContractTask(Box::new( - ContractTask::RemoveContract(contract_to_remove), - ))); - self.confirm_remove_contract_popup = false; - self.contract_to_remove = None; - } - - // Cancel button - if ui.button("Cancel").clicked() { - self.confirm_remove_contract_popup = false; - self.contract_to_remove = None; - } - }); + ), + ) + }); - // If user closes the popup window (the [x] button), also reset state - if !is_open { - self.confirm_remove_contract_popup = false; - self.contract_to_remove = None; + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + let action = AppAction::BackendTask(BackendTask::ContractTask(Box::new( + ContractTask::RemoveContract(contract_to_remove), + ))); + self.confirmation_dialog = None; + self.contract_to_remove = None; + action + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + self.contract_to_remove = None; + AppAction::None + } + None => AppAction::None, } - app_action } } @@ -692,6 +685,12 @@ impl ScreenLike for DocumentQueryScreen { RootScreenType::RootScreenDocumentQuery, ); + // Contracts sub-left panel: DPNS / Dashpay / Contracts (default) + action |= crate::ui::components::contracts_subscreen_chooser_panel::add_contracts_subscreen_chooser_panel( + ctx, + &self.app_context, + ); + action |= add_contract_chooser_panel( ctx, &mut self.contract_search_term, @@ -705,12 +704,13 @@ impl ScreenLike for DocumentQueryScreen { &mut self.contract_chooser_state, ); - if let AppAction::BackendTask(BackendTask::ContractTask(contract_task)) = &action { - if let ContractTask::RemoveContract(contract_id) = **contract_task { - action = AppAction::None; - self.confirm_remove_contract_popup = true; - self.contract_to_remove = Some(contract_id); - } + if let AppAction::BackendTask(BackendTask::ContractTask(contract_task)) = &action + && let ContractTask::RemoveContract(contract_id) = **contract_task + { + action = AppAction::None; + self.contract_to_remove = Some(contract_id); + // Clear any existing dialog to create a new one with updated content + self.confirmation_dialog = None; } // Custom central panel with adjusted margins for Document Query screen @@ -752,7 +752,7 @@ impl ScreenLike for DocumentQueryScreen { ); }); - if self.confirm_remove_contract_popup { + if self.contract_to_remove.is_some() { inner_action |= self.show_remove_contract_popup(ui); } inner_action @@ -780,10 +780,8 @@ fn doc_to_filtered_string( let mut filtered_map = serde_json::Map::new(); for (field_name, &is_checked) in selected_fields { - if is_checked { - if let Some(field_value) = obj.get(field_name) { - filtered_map.insert(field_name.clone(), field_value.clone()); - } + if is_checked && let Some(field_value) = obj.get(field_name) { + filtered_map.insert(field_name.clone(), field_value.clone()); } } diff --git a/src/ui/contracts_documents/dashpay_coming_soon_screen.rs b/src/ui/contracts_documents/dashpay_coming_soon_screen.rs new file mode 100644 index 000000000..d561ede58 --- /dev/null +++ b/src/ui/contracts_documents/dashpay_coming_soon_screen.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use eframe::egui::Context; + +use crate::app::AppAction; +use crate::context::AppContext; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::styled::island_central_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::{RootScreenType, ScreenLike}; + +pub struct DashpayScreen { + pub app_context: Arc, +} + +impl DashpayScreen { + pub fn new(app_context: &Arc) -> Self { + Self { + app_context: app_context.clone(), + } + } +} + +impl ScreenLike for DashpayScreen { + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Dashpay", AppAction::None), + ], + vec![], + ); + + // Keep Contracts highlighted in the main left panel + action |= add_left_panel( + ctx, + &self.app_context, + RootScreenType::RootScreenDocumentQuery, + ); + + // Contracts sub-left panel + action |= crate::ui::components::contracts_subscreen_chooser_panel::add_contracts_subscreen_chooser_panel( + ctx, + &self.app_context, + ); + + action |= island_central_panel(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.add_space(40.0); + ui.heading("Coming Soon"); + ui.add_space(20.0); + }); + AppAction::None + }); + + action + } +} diff --git a/src/ui/contracts_documents/document_action_screen.rs b/src/ui/contracts_documents/document_action_screen.rs index 3f9a3a477..67b39fd89 100644 --- a/src/ui/contracts_documents/document_action_screen.rs +++ b/src/ui/contracts_documents/document_action_screen.rs @@ -43,7 +43,7 @@ use dash_sdk::drive::query::WhereClause; use dash_sdk::platform::{DocumentQuery, Identifier, IdentityPublicKey}; use dash_sdk::query_types::IndexMap; use eframe::epaint::Color32; -use egui::{Context, PopupCloseBehavior, RichText, Ui}; +use egui::{Context, RichText, Ui}; use std::collections::{BTreeMap, HashMap}; use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -336,40 +336,42 @@ impl DocumentActionScreen { ui.label(&doc_id_str); let view_button_response = ui.button("View"); - if view_button_response.clicked() { - ui.memory_mut(|mem| { - mem.open_popup(egui::Id::new(format!("popup_{}", doc_id_str))) - }); - } + if ui.button("Select").clicked() { self.document_id_input = doc_id_str.clone(); } - egui::popup::popup_above_or_below_widget( - ui, - egui::Id::new(format!("popup_{}", doc_id_str)), + let popup_id = egui::Id::new(format!("popup_{}", doc_id_str)); + egui::Popup::new( + popup_id, + ui.ctx().clone(), &view_button_response, - egui::AboveOrBelow::Below, - PopupCloseBehavior::CloseOnClickOutside, - |ui| { - ui.set_min_width(400.0); - ui.label("Document JSON:"); - if let Ok(json) = serde_json::to_string_pretty(doc) { - ui.add( - egui::TextEdit::multiline(&mut json.clone()) - .font(egui::TextStyle::Monospace) - .desired_rows(10) - .desired_width(380.0) - .interactive(false), - ); - } else { - ui.label("Failed to serialize document."); - } - if ui.button("Close").clicked() { - ui.memory_mut(|mem| mem.close_popup()); - } - }, - ); + view_button_response.layer_id, + ) + .open_memory( + view_button_response + .clicked() + .then_some(egui::SetOpenCommand::Bool(true)), + ) + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) + .show(|ui| { + ui.set_min_width(400.0); + ui.label("Document JSON:"); + if let Ok(json) = serde_json::to_string_pretty(doc) { + ui.add( + egui::TextEdit::multiline(&mut json.clone()) + .font(egui::TextStyle::Monospace) + .desired_rows(10) + .desired_width(380.0) + .interactive(false), + ); + } else { + ui.label("Failed to serialize document."); + } + if ui.button("Close").clicked() { + ui.close(); + } + }); }); } else { ui.label("Document not found"); @@ -377,11 +379,11 @@ impl DocumentActionScreen { } } - if let Some(backend_message) = &self.backend_message { - if backend_message.contains("No owned documents found") { - ui.add_space(10.0); - ui.label("No owned documents found."); - } + if let Some(backend_message) = &self.backend_message + && backend_message.contains("No owned documents found") + { + ui.add_space(10.0); + ui.label("No owned documents found."); } // Show fetching status @@ -1242,6 +1244,7 @@ impl DocumentActionScreen { id, properties, owner_id, + creator_id: None, revision, created_at: None, updated_at: None, @@ -1410,6 +1413,7 @@ impl DocumentActionScreen { id: original_doc.id(), properties, owner_id: original_doc.owner_id(), + creator_id: original_doc.creator_id(), revision: new_revision, created_at: None, updated_at: None, @@ -1468,6 +1472,12 @@ impl ScreenLike for DocumentActionScreen { crate::ui::RootScreenType::RootScreenDocumentQuery, ); + // Contracts sub-left panel + action |= crate::ui::components::contracts_subscreen_chooser_panel::add_contracts_subscreen_chooser_panel( + ctx, + &self.app_context, + ); + action |= island_central_panel(ctx, |ui| match &self.broadcast_status { BroadcastStatus::Broadcasted => { let success_message = format!("{} successful!", self.action_type.display_name()); diff --git a/src/ui/contracts_documents/group_actions_screen.rs b/src/ui/contracts_documents/group_actions_screen.rs index 1217ff485..3c1f3e7c5 100644 --- a/src/ui/contracts_documents/group_actions_screen.rs +++ b/src/ui/contracts_documents/group_actions_screen.rs @@ -12,13 +12,14 @@ use crate::app::AppAction; use crate::backend_task::contract::ContractTask; use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; use crate::context::AppContext; +use crate::model::amount::Amount; use crate::model::qualified_contract::QualifiedContract; use crate::model::qualified_identity::QualifiedIdentity; +use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::helpers::add_contract_chooser_pre_filtered; -use crate::ui::helpers::render_identity_selector; use crate::ui::tokens::burn_tokens_screen::BurnTokensScreen; use crate::ui::tokens::destroy_frozen_funds_screen::DestroyFrozenFundsScreen; use crate::ui::tokens::freeze_tokens_screen::FreezeTokensScreen; @@ -78,6 +79,7 @@ pub struct GroupActionsScreen { qualified_identities: Vec, identity_token_balances: IndexMap, selected_identity: Option, + selected_identity_str: String, // Backend task status fetch_group_actions_status: FetchGroupActionsStatus, @@ -142,6 +144,7 @@ impl GroupActionsScreen { qualified_identities, identity_token_balances, selected_identity: None, + selected_identity_str: String::new(), // Backend task status fetch_group_actions_status: FetchGroupActionsStatus::NotStarted, @@ -353,14 +356,22 @@ impl GroupActionsScreen { TokenEvent::Mint(amount, _identifier, note_opt) => { let mut mint_screen = MintTokensScreen::new(identity_token_info, &self.app_context); mint_screen.group_action_id = Some(action_id); - mint_screen.amount_to_mint = amount.to_string(); + // Convert amount to Amount struct using the token configuration + mint_screen.amount = Some(Amount::from_token( + &mint_screen.identity_token_info, + *amount, + )); mint_screen.public_note = note_opt.clone(); *action |= AppAction::AddScreen(Screen::MintTokensScreen(mint_screen)); } TokenEvent::Burn(amount, _burn_from, note_opt) => { let mut burn_screen = BurnTokensScreen::new(identity_token_info, &self.app_context); burn_screen.group_action_id = Some(action_id); - burn_screen.amount_to_burn = amount.to_string(); + // Convert amount to Amount struct using the token configuration + burn_screen.amount = Some(Amount::from_token( + &burn_screen.identity_token_info, + *amount, + )); burn_screen.public_note = note_opt.clone(); *action |= AppAction::AddScreen(Screen::BurnTokensScreen(burn_screen)); } @@ -418,7 +429,9 @@ impl GroupActionsScreen { } TokenEvent::ChangePriceForDirectPurchase(schedule, note_opt) => { let mut change_price_screen = - SetTokenPriceScreen::new(identity_token_info, &self.app_context); + SetTokenPriceScreen::new(identity_token_info, &self.app_context) + .with_schedule(schedule.clone()); + change_price_screen.group_action_id = Some(action_id); change_price_screen.token_pricing_schedule = format!("{:?}", schedule); change_price_screen.public_note = note_opt.clone(); @@ -478,6 +491,12 @@ impl ScreenLike for GroupActionsScreen { RootScreenType::RootScreenDocumentQuery, ); + // Contracts sub-left panel + action |= crate::ui::components::contracts_subscreen_chooser_panel::add_contracts_subscreen_chooser_panel( + ctx, + &self.app_context, + ); + let central_panel_action = island_central_panel(ctx, |ui| { ui.heading("Active Group Actions"); @@ -523,8 +542,19 @@ impl ScreenLike for GroupActionsScreen { ui.heading("2. Select an identity:"); ui.add_space(10.0); - self.selected_identity = - render_identity_selector(ui, &self.qualified_identities, &self.selected_identity); + + ui.add( + IdentitySelector::new( + "group_actions_identity_selector", + &mut self.selected_identity_str, + &self.qualified_identities, + ) + .selected_identity(&mut self.selected_identity) + .expect("Failed to create identity selector") + .other_option(false) + .width(250.0) + .label("Identity:"), + ); let mut fetch_clicked = false; if self.selected_contract.is_some() && self.selected_identity.is_some() { @@ -579,15 +609,15 @@ impl ScreenLike for GroupActionsScreen { _ => {} } - if fetch_clicked { - if let (Some(contract), Some(identity)) = ( + if fetch_clicked + && let (Some(contract), Some(identity)) = ( self.selected_contract.clone(), self.selected_identity.clone(), - ) { - action |= AppAction::BackendTask(BackendTask::ContractTask(Box::new( - ContractTask::FetchActiveGroupActions(contract, identity), - ))); - } + ) + { + action |= AppAction::BackendTask(BackendTask::ContractTask(Box::new( + ContractTask::FetchActiveGroupActions(contract, identity), + ))); } if let FetchGroupActionsStatus::Complete(group_actions) = diff --git a/src/ui/contracts_documents/mod.rs b/src/ui/contracts_documents/mod.rs index 47ce18225..07011f768 100644 --- a/src/ui/contracts_documents/mod.rs +++ b/src/ui/contracts_documents/mod.rs @@ -1,5 +1,6 @@ pub mod add_contracts_screen; pub mod contracts_documents_screen; +pub mod dashpay_coming_soon_screen; pub mod document_action_screen; pub mod group_actions_screen; pub mod register_contract_screen; diff --git a/src/ui/contracts_documents/register_contract_screen.rs b/src/ui/contracts_documents/register_contract_screen.rs index 2aa2f2f42..b8386ffb6 100644 --- a/src/ui/contracts_documents/register_contract_screen.rs +++ b/src/ui/contracts_documents/register_contract_screen.rs @@ -205,15 +205,15 @@ impl RegisterDataContractScreen { } } - if let AppAction::BackendTask(BackendTask::ContractTask(contract_task)) = &app_action { - if let ContractTask::RegisterDataContract(_, _, _, _) = **contract_task { - self.broadcast_status = BroadcastStatus::Broadcasting( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(), - ); - } + if let AppAction::BackendTask(BackendTask::ContractTask(contract_task)) = &app_action + && let ContractTask::RegisterDataContract(_, _, _, _) = **contract_task + { + self.broadcast_status = BroadcastStatus::Broadcasting( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ); } app_action @@ -322,6 +322,12 @@ impl ScreenLike for RegisterDataContractScreen { crate::ui::RootScreenType::RootScreenDocumentQuery, ); + // Contracts sub-left panel + action |= crate::ui::components::contracts_subscreen_chooser_panel::add_contracts_subscreen_chooser_panel( + ctx, + &self.app_context, + ); + action |= island_central_panel(ctx, |ui| { if self.broadcast_status == BroadcastStatus::Done { return self.show_success(ui); diff --git a/src/ui/contracts_documents/update_contract_screen.rs b/src/ui/contracts_documents/update_contract_screen.rs index bc1cb1f6c..6e9bc0c33 100644 --- a/src/ui/contracts_documents/update_contract_screen.rs +++ b/src/ui/contracts_documents/update_contract_screen.rs @@ -248,15 +248,15 @@ impl UpdateDataContractScreen { } } - if let AppAction::BackendTask(BackendTask::ContractTask(contract_task)) = &app_action { - if let ContractTask::UpdateDataContract(_, _, _) = **contract_task { - self.broadcast_status = BroadcastStatus::FetchingNonce( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(), - ); - } + if let AppAction::BackendTask(BackendTask::ContractTask(contract_task)) = &app_action + && let ContractTask::UpdateDataContract(_, _, _) = **contract_task + { + self.broadcast_status = BroadcastStatus::FetchingNonce( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ); } app_action @@ -364,6 +364,12 @@ impl ScreenLike for UpdateDataContractScreen { crate::ui::RootScreenType::RootScreenDocumentQuery, ); + // Contracts sub-left panel + action |= crate::ui::components::contracts_subscreen_chooser_panel::add_contracts_subscreen_chooser_panel( + ctx, + &self.app_context, + ); + action |= island_central_panel(ctx, |ui| { if self.broadcast_status == BroadcastStatus::Done { return self.show_success(ui); diff --git a/src/ui/dpns/dpns_contested_names_screen.rs b/src/ui/dpns/dpns_contested_names_screen.rs index b7fb9266f..0a6d66804 100644 --- a/src/ui/dpns/dpns_contested_names_screen.rs +++ b/src/ui/dpns/dpns_contested_names_screen.rs @@ -17,9 +17,10 @@ use crate::backend_task::identity::IdentityTask; use crate::context::AppContext; use crate::model::contested_name::{ContestState, ContestedName}; use crate::model::qualified_identity::{DPNSNameInfo, QualifiedIdentity}; +use crate::ui::components::contracts_subscreen_chooser_panel::add_contracts_subscreen_chooser_panel; use crate::ui::components::dpns_subscreen_chooser_panel::add_dpns_subscreen_chooser_panel; use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::island_central_panel; +use crate::ui::components::styled::{StyledButton, island_central_panel}; use crate::ui::components::top_panel::add_top_panel; use crate::ui::theme::DashColors; use crate::ui::{BackendTaskSuccessResult, MessageType, RootScreenType, ScreenLike, ScreenType}; @@ -304,7 +305,7 @@ impl DPNSScreen { let dark_mode = ui.ctx().style().visuals.dark_mode; ui.label(RichText::new("Please check back later or try refreshing the list.").color(DashColors::text_primary(dark_mode))); ui.add_space(20.0); - if ui.button("Refresh").clicked() { + if StyledButton::primary("Refresh").show(ui).clicked() { if let RefreshingStatus::Refreshing(_) = self.refreshing_status { app_action = AppAction::None; } else { @@ -383,15 +384,15 @@ impl DPNSScreen { .striped(false) .resizable(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) - .column(Column::initial(200.0).resizable(true)) // Contested Name - .column(Column::initial(100.0).resizable(true)) // Locked - .column(Column::initial(100.0).resizable(true)) // Abstain - .column(Column::initial(200.0).resizable(true)) // Ending Time - .column(Column::initial(200.0).resizable(true)) // Last Updated - .column(Column::remainder()) // Contestants + .column(Column::auto().resizable(true)) // Contested Name + .column(Column::auto().resizable(true)) // Locked + .column(Column::auto().resizable(true)) // Abstain + .column(Column::auto().resizable(true)) // Ending Time + .column(Column::auto().resizable(true)) // Last Updated + .column(Column::auto().resizable(true)) // Contestants .header(30.0, |mut header| { header.col(|ui| { - if ui.button("Contested Name").clicked() { + if ui.button("Name").clicked() { self.toggle_sort(SortColumn::ContestedName); } }); @@ -491,10 +492,13 @@ impl DPNSScreen { // LOCK button row.col(|ui| { let label_text = format!("{}", locked_votes); + let dark_green = Color32::from_rgb(0, 100, 0); + let dark_mode = ui.ctx().style().visuals.dark_mode; + let normal_color = DashColors::text_primary(dark_mode); let text_widget = if is_locked_votes_bold { - RichText::new(label_text).strong() + RichText::new(label_text).strong().color(dark_green) } else { - RichText::new(label_text) + RichText::new(label_text).color(normal_color) }; // See if this (LOCK) is selected @@ -708,13 +712,13 @@ impl DPNSScreen { .striped(false) .resizable(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) - .column(Column::initial(200.0).resizable(true)) // Name - .column(Column::initial(200.0).resizable(true)) // Ended Time - .column(Column::initial(200.0).resizable(true)) // Last Updated - .column(Column::initial(200.0).resizable(true)) // Awarded To + .column(Column::auto().resizable(true)) // Name + .column(Column::auto().resizable(true)) // Ended Time + .column(Column::auto().resizable(true)) // Last Updated + .column(Column::auto().resizable(true)) // Awarded To .header(30.0, |mut header| { header.col(|ui| { - if ui.button("Contested Name").clicked() { + if ui.button("Name").clicked() { self.toggle_sort(SortColumn::ContestedName); } }); @@ -895,9 +899,9 @@ impl DPNSScreen { .striped(false) .resizable(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) - .column(Column::initial(200.0).resizable(true)) // DPNS Name - .column(Column::initial(400.0).resizable(true)) // Owner ID - .column(Column::initial(300.0).resizable(true)) // Acquired At + .column(Column::auto().resizable(true)) // DPNS Name + .column(Column::auto().resizable(true)) // Owner ID + .column(Column::auto().resizable(true)) // Acquired At .header(30.0, |mut header| { header.col(|ui| { if ui.button("Name").clicked() { @@ -972,15 +976,15 @@ impl DPNSScreen { .striped(false) .resizable(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) - .column(Column::initial(100.0).resizable(true)) // ContestedName - .column(Column::initial(200.0).resizable(true)) // Voter - .column(Column::initial(200.0).resizable(true)) // Choice - .column(Column::initial(200.0).resizable(true)) // Time - .column(Column::initial(100.0).resizable(true)) // Status - .column(Column::initial(100.0).resizable(true)) // Actions + .column(Column::auto().resizable(true)) // ContestedName + .column(Column::auto().resizable(true)) // Voter + .column(Column::auto().resizable(true)) // Choice + .column(Column::auto().resizable(true)) // Time + .column(Column::auto().resizable(true)) // Status + .column(Column::auto().resizable(true)) // Actions .header(30.0, |mut header| { header.col(|ui| { - if ui.button("Contested Name").clicked() { + if ui.button("Name").clicked() { self.toggle_sort(SortColumn::ContestedName); } }); @@ -1117,13 +1121,13 @@ impl DPNSScreen { vote.1 = ScheduledVoteCastingStatus::InProgress; // Mark in our Arc as well - if let Ok(mut sched_guard) = self.scheduled_votes.lock() { - if let Some(t) = sched_guard.iter_mut().find(|(sv, _)| { + if let Ok(mut sched_guard) = self.scheduled_votes.lock() + && let Some(t) = sched_guard.iter_mut().find(|(sv, _)| { sv.voter_id == vote.0.voter_id && sv.contested_name == vote.0.contested_name - }) { - t.1 = ScheduledVoteCastingStatus::InProgress; - } + }) + { + t.1 = ScheduledVoteCastingStatus::InProgress; } // dispatch the actual cast let local_ids = @@ -1876,12 +1880,12 @@ impl ScreenLike for DPNSScreen { } } BackendTaskSuccessResult::CastScheduledVote(vote) => { - if let Ok(mut guard) = self.scheduled_votes.lock() { - if let Some((_, status)) = guard.iter_mut().find(|(v, _)| { + if let Ok(mut guard) = self.scheduled_votes.lock() + && let Some((_, status)) = guard.iter_mut().find(|(v, _)| { v.contested_name == vote.contested_name && v.voter_id == vote.voter_id - }) { - *status = ScheduledVoteCastingStatus::Completed; - } + }) + { + *status = ScheduledVoteCastingStatus::Completed; } } _ => {} @@ -2014,7 +2018,10 @@ impl ScreenLike for DPNSScreen { } } - // Subscreen chooser + // Contracts area chooser (DPNS / Dashpay / Contracts) + action |= add_contracts_subscreen_chooser_panel(ctx, self.app_context.as_ref()); + + // DPNS subscreen chooser action |= add_dpns_subscreen_chooser_panel(ctx, self.app_context.as_ref()); // Main panel @@ -2144,10 +2151,10 @@ impl ScreenLike for DPNSScreen { } // If we have a pending backend task from scheduling (e.g. after immediate votes) - if action == AppAction::None { - if let Some(bt) = self.pending_backend_task.take() { - action = AppAction::BackendTask(bt); - } + if action == AppAction::None + && let Some(bt) = self.pending_backend_task.take() + { + action = AppAction::BackendTask(bt); } action } diff --git a/src/ui/helpers.rs b/src/ui/helpers.rs index 395a8ccf6..7edafbb4f 100644 --- a/src/ui/helpers.rs +++ b/src/ui/helpers.rs @@ -24,6 +24,10 @@ use egui::{Color32, ComboBox, Response, Ui}; use super::tokens::tokens_screen::IdentityTokenInfo; +/// Layout of labels and buttons in the UI fails to vertically align properly containers that contain buttons and other items (labels, text fields, etc.). +/// This constant provides a constant padding to be used in such cases to ensure proper alignment. +pub const BUTTON_ADJUSTMENT_PADDING_TOP: f32 = 15.0; + /// Helper function to create a styled info icon button pub fn info_icon_button(ui: &mut egui::Ui, hover_text: &str) -> Response { let (rect, response) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::click()); @@ -54,49 +58,6 @@ pub fn info_icon_button(ui: &mut egui::Ui, hover_text: &str) -> Response { response.on_hover_text(hover_text) } -/// Returns the newly selected identity (if changed), otherwise the existing one. -pub fn render_identity_selector( - ui: &mut Ui, - qualified_identities: &[QualifiedIdentity], - selected_identity: &Option, -) -> Option { - let mut new_selected_identity = selected_identity.clone(); - - ui.horizontal(|ui| { - ui.label("Identity:"); - ComboBox::from_id_salt("identity_selector") - .selected_text( - selected_identity - .as_ref() - .map(|qi| { - qi.alias - .as_ref() - .unwrap_or(&qi.identity.id().to_string(Encoding::Base58)) - .clone() - }) - .unwrap_or_else(|| "Choose identity…".into()), - ) - .show_ui(ui, |cb| { - for qi in qualified_identities { - let label = qi - .alias - .as_ref() - .unwrap_or(&qi.identity.id().to_string(Encoding::Base58)) - .clone(); - - if cb - .selectable_label(selected_identity.as_ref() == Some(qi), label) - .clicked() - { - new_selected_identity = Some(qi.clone()); - } - } - }); - }); - - new_selected_identity -} - /// Returns the newly selected key (if changed), otherwise the existing one. // Allow dead_code: This function provides UI for key selection within identities, // useful for identity-based operations and key management interfaces @@ -290,10 +251,11 @@ pub fn add_identity_key_chooser_with_doc_type<'a, T>( .as_ref() .map(|k| { format!( - "Key {} Type {} Security {}", + "Key {} | {} | {} | {}", k.id(), - k.key_type(), - k.security_level() + k.purpose(), + k.security_level(), + k.key_type() ) }) .unwrap_or_else(|| "Select Key…".into()), @@ -303,23 +265,25 @@ pub fn add_identity_key_chooser_with_doc_type<'a, T>( let allowed_purposes = transaction_type.allowed_purposes(); let allowed_security_levels = if transaction_type == TransactionType::DocumentAction - && document_type.is_some() { - // For document actions with a specific document type, use its security requirement - let required_level = - document_type.unwrap().security_level_requirement(); - let allowed_levels = - SecurityLevel::CRITICAL as u8..=required_level as u8; - let allowed_levels: Vec = [ - SecurityLevel::CRITICAL, - SecurityLevel::HIGH, - SecurityLevel::MEDIUM, - ] - .iter() - .cloned() - .filter(|level| allowed_levels.contains(&(*level as u8))) - .collect(); - allowed_levels + if let Some(document_type) = document_type { + // For document actions with a specific document type, use its security requirement + let required_level = document_type.security_level_requirement(); + let allowed_levels = + SecurityLevel::CRITICAL as u8..=required_level as u8; + let allowed_levels: Vec = [ + SecurityLevel::CRITICAL, + SecurityLevel::HIGH, + SecurityLevel::MEDIUM, + ] + .iter() + .cloned() + .filter(|level| allowed_levels.contains(&(*level as u8))) + .collect(); + allowed_levels + } else { + transaction_type.allowed_security_levels() + } } else { transaction_type.allowed_security_levels() }; @@ -344,15 +308,19 @@ pub fn add_identity_key_chooser_with_doc_type<'a, T>( { // In dev mode, mark keys that wouldn't normally be allowed format!( - "Key {} Security {} [DEV]", + "Key {} | {} | {} | {} [DEV]", key.id(), - key.security_level() + key.purpose(), + key.security_level(), + key.key_type() ) } else { format!( - "Key {} Security {}", + "Key {} | {} | {} | {}", key.id(), - key.security_level() + key.purpose(), + key.security_level(), + key.key_type() ) }; diff --git a/src/ui/identities/add_existing_identity_screen.rs b/src/ui/identities/add_existing_identity_screen.rs index 4b34b8947..8348fcf8f 100644 --- a/src/ui/identities/add_existing_identity_screen.rs +++ b/src/ui/identities/add_existing_identity_screen.rs @@ -54,6 +54,18 @@ fn load_testnet_nodes_from_yml(file_path: &str) -> Option { serde_yaml::from_str(&file_content).expect("expected proper yaml") } +#[derive(Clone, Copy, PartialEq, Eq)] +enum LoadIdentityMode { + ByIdentityId, + ByWallet, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum WalletIdentitySearchMode { + SpecificIndex, + UpToIndex, +} + #[derive(PartialEq)] pub enum AddIdentityStatus { NotStarted, @@ -79,6 +91,10 @@ pub struct AddExistingIdentityScreen { pub identity_index_input: String, pub app_context: Arc, show_pop_up_info: Option, + mode: LoadIdentityMode, + backend_message: Option, + wallet_search_mode: WalletIdentitySearchMode, + success_message: Option, } impl AddExistingIdentityScreen { @@ -106,6 +122,10 @@ impl AddExistingIdentityScreen { identity_index_input: String::new(), app_context: app_context.clone(), show_pop_up_info: None, + mode: LoadIdentityMode::ByIdentityId, + backend_message: None, + wallet_search_mode: WalletIdentitySearchMode::SpecificIndex, + success_message: None, } } @@ -254,7 +274,7 @@ impl AddExistingIdentityScreen { action } - fn _render_wallet_selection(&mut self, ui: &mut Ui) { + fn render_wallet_selection(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { if self.app_context.has_wallet.load(Ordering::Relaxed) { let wallets = &self.app_context.wallets.read().unwrap(); @@ -305,15 +325,24 @@ impl AddExistingIdentityScreen { }); } - fn _render_from_wallet(&mut self, ui: &mut egui::Ui, wallets_len: usize) -> AppAction { + fn render_by_wallet(&mut self, ui: &mut egui::Ui, wallets_len: usize) -> AppAction { let mut action = AppAction::None; + if wallets_len == 0 { + ui.colored_label( + Color32::GRAY, + "No wallets available. Import or create a wallet to search by derivation path.", + ); + return action; + } + // Wallet selection if wallets_len > 1 { - self._render_wallet_selection(ui); + self.render_wallet_selection(ui); } if self.selected_wallet.is_none() { + ui.label("Select a wallet to search for linked identities."); return action; }; @@ -323,26 +352,79 @@ impl AddExistingIdentityScreen { return action; } - // Identity index input + let mut wallet_mode_changed = false; ui.horizontal(|ui| { - ui.label("Identity Index:"); + ui.label("Search type:"); + wallet_mode_changed |= ui + .selectable_value( + &mut self.wallet_search_mode, + WalletIdentitySearchMode::SpecificIndex, + "Specific index", + ) + .changed(); + wallet_mode_changed |= ui + .selectable_value( + &mut self.wallet_search_mode, + WalletIdentitySearchMode::UpToIndex, + "All up to index", + ) + .changed(); + }); + if wallet_mode_changed { + self.add_identity_status = AddIdentityStatus::NotStarted; + self.error_message = None; + self.backend_message = None; + self.success_message = None; + } + ui.add_space(6.0); + + let identity_index_label = match self.wallet_search_mode { + WalletIdentitySearchMode::SpecificIndex => "Identity index:", + WalletIdentitySearchMode::UpToIndex => "Highest identity index to search (inclusive):", + }; + + ui.horizontal(|ui| { + ui.label(identity_index_label); ui.text_edit_singleline(&mut self.identity_index_input); }); - if ui.button("Search For Identity").clicked() { + match self.wallet_search_mode { + WalletIdentitySearchMode::SpecificIndex => { + ui.label("This is the derivation index used when the identity was created."); + } + WalletIdentitySearchMode::UpToIndex => { + ui.label( + "Searches each derivation index starting at 0 up to the provided index (inclusive).", + ); + } + } + + let button_label = match self.wallet_search_mode { + WalletIdentitySearchMode::SpecificIndex => "Search For Identity", + WalletIdentitySearchMode::UpToIndex => "Load Identities", + }; + + if ui.button(button_label).clicked() { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_secs(); self.add_identity_status = AddIdentityStatus::WaitingForResult(now); + self.backend_message = None; + self.success_message = None; // Parse identity index input if let Ok(identity_index) = self.identity_index_input.trim().parse::() { + let wallet_ref = self.selected_wallet.as_ref().unwrap().clone().into(); action = AppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::SearchIdentityFromWallet( - self.selected_wallet.as_ref().unwrap().clone().into(), - identity_index, - ), + match self.wallet_search_mode { + WalletIdentitySearchMode::SpecificIndex => { + IdentityTask::SearchIdentityFromWallet(wallet_ref, identity_index) + } + WalletIdentitySearchMode::UpToIndex => { + IdentityTask::SearchIdentitiesUpToIndex(wallet_ref, identity_index) + } + }, )); } else { // Handle invalid index input (optional) @@ -411,7 +493,11 @@ impl AddExistingIdentityScreen { ui.add_space(50.0); ui.heading("🎉"); - ui.heading("Successfully loaded identity."); + let success_text = self + .success_message + .clone() + .unwrap_or_else(|| "Successfully loaded identity.".to_string()); + ui.label(RichText::new(success_text)); ui.add_space(20.0); @@ -426,6 +512,8 @@ impl AddExistingIdentityScreen { self.error_message = None; self.show_pop_up_info = None; self.add_identity_status = AddIdentityStatus::NotStarted; + self.backend_message = None; + self.success_message = None; } ui.add_space(5.0); @@ -474,7 +562,18 @@ impl ScreenLike for AddExistingIdentityScreen { match message_type { MessageType::Success => { if message == "Successfully loaded identity" { + self.success_message = Some("Successfully loaded identity.".to_string()); self.add_identity_status = AddIdentityStatus::Complete; + self.backend_message = None; + } else if (message.starts_with("Successfully loaded ") + && message.contains(" up to index ")) + || message.starts_with("Finished loading identities up to index ") + { + self.success_message = Some(message.to_string()); + self.add_identity_status = AddIdentityStatus::Complete; + self.backend_message = None; + } else { + self.backend_message = Some(message.to_string()); } } MessageType::Info => {} @@ -520,7 +619,44 @@ impl ScreenLike for AddExistingIdentityScreen { return; } - inner_action |= self.render_by_identity(ui); + let mut mode_changed = false; + ui.horizontal(|ui| { + mode_changed |= ui + .selectable_value( + &mut self.mode, + LoadIdentityMode::ByIdentityId, + "By Identity", + ) + .changed(); + mode_changed |= ui + .selectable_value( + &mut self.mode, + LoadIdentityMode::ByWallet, + "By Wallet", + ) + .changed(); + }); + ui.add_space(10.0); + + if mode_changed { + self.add_identity_status = AddIdentityStatus::NotStarted; + self.error_message = None; + self.backend_message = None; + self.success_message = None; + } + + match self.mode { + LoadIdentityMode::ByIdentityId => { + inner_action |= self.render_by_identity(ui); + } + LoadIdentityMode::ByWallet => { + let wallets_len = { + let wallets = self.app_context.wallets.read().unwrap(); + wallets.len() + }; + inner_action |= self.render_by_wallet(ui, wallets_len); + } + } ui.add_space(10.0); @@ -554,6 +690,10 @@ impl ScreenLike for AddExistingIdentityScreen { }; ui.label(format!("Loading... Time taken so far: {}", display_time)); + + if self.backend_message.is_some() { + ui.label(self.backend_message.clone().unwrap().to_string()); + } } AddIdentityStatus::ErrorMessage(msg) => { ui.colored_label(egui::Color32::DARK_RED, format!("Error: {}", msg)); diff --git a/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs b/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs index 628dd333f..aea781aeb 100644 --- a/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs +++ b/src/ui/identities/add_new_identity_screen/by_using_unused_asset_lock.rs @@ -102,18 +102,14 @@ impl AddNewIdentityScreen { ui.add_space(20.0); } - ui.vertical_centered(|ui| { - match step { - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - ui.heading("=> Waiting for Platform acknowledgement <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::Success => { - ui.heading("...Success..."); - } - _ => {} + ui.vertical_centered(|ui| match step { + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("=> Waiting for Platform acknowledgement <="); } + WalletFundedScreenStep::Success => { + ui.heading("...Success..."); + } + _ => {} }); ui.add_space(40.0); diff --git a/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs b/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs index fe6c25daf..81612bedc 100644 --- a/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs +++ b/src/ui/identities/add_new_identity_screen/by_using_unused_balance.rs @@ -61,23 +61,17 @@ impl AddNewIdentityScreen { ui.add_space(20.0); } - ui.vertical_centered(|ui| { - match step { - WalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - ui.heading("=> Waiting for Platform acknowledgement <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::Success => { - ui.heading("...Success..."); - } - _ => {} + ui.vertical_centered(|ui| match step { + WalletFundedScreenStep::WaitingForAssetLock => { + ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); } + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("=> Waiting for Platform acknowledgement <="); + } + WalletFundedScreenStep::Success => { + ui.heading("...Success..."); + } + _ => {} }); ui.add_space(40.0); diff --git a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs index 14b905714..c3c9c3455 100644 --- a/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs +++ b/src/ui/identities/add_new_identity_screen/by_wallet_qr_code.rs @@ -6,7 +6,7 @@ use crate::backend_task::identity::{ use crate::ui::identities::add_new_identity_screen::{ AddNewIdentityScreen, WalletFundedScreenStep, }; -use crate::ui::identities::funding_common::{copy_to_clipboard, generate_qr_code_image}; +use crate::ui::identities::funding_common::{self, copy_to_clipboard, generate_qr_code_image}; use dash_sdk::dashcore_rpc::RpcApi; use eframe::epaint::TextureHandle; use egui::{Color32, Ui}; @@ -119,6 +119,15 @@ impl AddNewIdentityScreen { } pub fn render_ui_by_wallet_qr_code(&mut self, ui: &mut Ui, step_number: u32) -> AppAction { + // Update state when funds land on the QR funding address + if let Some(utxo) = funding_common::capture_qr_funding_utxo_if_available( + &self.step, + self.selected_wallet.as_ref(), + self.funding_address.as_ref(), + ) { + self.funding_utxo = Some(utxo); + } + // Extract the step from the RwLock to minimize borrow scope let step = *self.step.read().unwrap(); @@ -136,6 +145,11 @@ impl AddNewIdentityScreen { self.render_funding_amount_input(ui); + if step == WalletFundedScreenStep::WaitingOnFunds { + ui.ctx() + .request_repaint_after(std::time::Duration::from_secs(1)); + } + let Ok(amount_dash) = self.funding_amount.parse::() else { return AppAction::None; }; @@ -148,65 +162,65 @@ impl AddNewIdentityScreen { egui::Layout::top_down(egui::Align::Min).with_cross_align(egui::Align::Center), |ui| { if let Err(e) = self.render_qr_code(ui, amount_dash) { - self.error_message = Some(e); - } - - ui.add_space(20.0); + self.error_message = Some(e); + } - if let Some(error_message) = self.error_message.as_ref() { - ui.colored_label(Color32::DARK_RED, error_message); ui.add_space(20.0); - } - match step { - WalletFundedScreenStep::ChooseFundingMethod => {} - WalletFundedScreenStep::WaitingOnFunds => { - ui.heading("=> Waiting for funds. <="); + if let Some(error_message) = self.error_message.as_ref() { + ui.colored_label(Color32::DARK_RED, error_message); + ui.add_space(20.0); } - WalletFundedScreenStep::FundsReceived => { - let Some(selected_wallet) = &self.selected_wallet else { - return AppAction::None; - }; - if let Some((utxo, tx_out, address)) = self.funding_utxo.clone() { - let identity_input = IdentityRegistrationInfo { - alias_input: self.alias_input.clone(), - keys: self.identity_keys.clone(), - wallet: Arc::clone(selected_wallet), // Clone the Arc reference - wallet_identity_index: self.identity_id_number, - identity_funding_method: RegisterIdentityFundingMethod::FundWithUtxo( - utxo, - tx_out, - address, - self.identity_id_number, - ), - }; - - let mut step = self.step.write().unwrap(); - *step = WalletFundedScreenStep::WaitingForAssetLock; - // Create the backend task to register the identity - return AppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::RegisterIdentity(identity_input), - )) + match step { + WalletFundedScreenStep::ChooseFundingMethod => {} + WalletFundedScreenStep::WaitingOnFunds => { + ui.heading("=> Waiting for funds. <="); + } + WalletFundedScreenStep::FundsReceived => { + let Some(selected_wallet) = &self.selected_wallet else { + return AppAction::None; + }; + if let Some((utxo, tx_out, address)) = self.funding_utxo.clone() { + let identity_input = IdentityRegistrationInfo { + alias_input: self.alias_input.clone(), + keys: self.identity_keys.clone(), + wallet: Arc::clone(selected_wallet), // Clone the Arc reference + wallet_identity_index: self.identity_id_number, + identity_funding_method: + RegisterIdentityFundingMethod::FundWithUtxo( + utxo, + tx_out, + address, + self.identity_id_number, + ), + }; + + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::WaitingForAssetLock; + + // Create the backend task to register the identity + return AppAction::BackendTask(BackendTask::IdentityTask( + IdentityTask::RegisterIdentity(identity_input), + )); + } + } + WalletFundedScreenStep::ReadyToCreate => {} + WalletFundedScreenStep::WaitingForAssetLock => { + ui.heading( + "=> Waiting for Core Chain to produce proof of transfer of funds. <=", + ); + } + WalletFundedScreenStep::WaitingForPlatformAcceptance => { + ui.heading("=> Waiting for Platform acknowledgement. <="); + } + WalletFundedScreenStep::Success => { + ui.heading("...Success..."); } } - WalletFundedScreenStep::ReadyToCreate => {} - WalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - ui.heading("=> Waiting for Platform acknowledgement. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); - } - WalletFundedScreenStep::Success => { - ui.heading("...Success..."); - } - } - AppAction::None - }); + AppAction::None + }, + ); ui.add_space(40.0); diff --git a/src/ui/identities/add_new_identity_screen/mod.rs b/src/ui/identities/add_new_identity_screen/mod.rs index d795d6a04..040b1a540 100644 --- a/src/ui/identities/add_new_identity_screen/mod.rs +++ b/src/ui/identities/add_new_identity_screen/mod.rs @@ -28,7 +28,7 @@ use dash_sdk::dpp::prelude::AssetLockProof; use dash_sdk::platform::Identifier; use eframe::egui::Context; use egui::ahash::HashSet; -use egui::{Color32, ComboBox, ScrollArea, Ui}; +use egui::{Button, Color32, ComboBox, ScrollArea, Ui}; use std::cmp::PartialEq; use std::fmt; use std::sync::atomic::Ordering; @@ -255,10 +255,8 @@ impl AddNewIdentityScreen { let enabled = !is_used || is_selected; // Use `add_enabled` to disable used indices - let response = ui.add_enabled( - enabled, - egui::SelectableLabel::new(is_selected, label), - ); + let response = + ui.add_enabled(enabled, Button::selectable(is_selected, label)); // Only allow selection if the index is not used if response.clicked() && !is_used { @@ -373,6 +371,17 @@ impl AddNewIdentityScreen { if ui.selectable_label(is_selected, wallet_alias).clicked() { // Update the selected wallet selected_wallet = Some(wallet.clone()); + // Reset the funding address + self.funding_address = None; + // Reset the funding asset lock + self.funding_asset_lock = None; + // Reset the funding UTXO + self.funding_utxo = None; + // Reset the copied to clipboard state + self.copied_to_clipboard = None; + // Reset the step to choose funding method + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::ChooseFundingMethod; } } }); @@ -879,20 +888,29 @@ impl ScreenLike for AddNewIdentityScreen { } } fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + if let BackendTaskSuccessResult::RegisteredIdentity(qualified_identity) = + &backend_task_success_result + { + self.successful_qualified_identity_id = Some(qualified_identity.identity.id()); + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::Success; + return; + } + let mut step = self.step.write().unwrap(); - match *step { + let current_step = *step; + match current_step { WalletFundedScreenStep::ChooseFundingMethod => {} WalletFundedScreenStep::WaitingOnFunds => { - if let Some(funding_address) = self.funding_address.as_ref() { - if let BackendTaskSuccessResult::CoreItem( + if let Some(funding_address) = self.funding_address.as_ref() + && let BackendTaskSuccessResult::CoreItem( CoreItem::ReceivedAvailableUTXOTransaction(_, outpoints_with_addresses), - ) = backend_task_success_result - { - for (outpoint, tx_out, address) in outpoints_with_addresses { - if funding_address == &address { - *step = WalletFundedScreenStep::FundsReceived; - self.funding_utxo = Some((outpoint, tx_out, address)) - } + ) = &backend_task_success_result + { + for (outpoint, tx_out, address) in outpoints_with_addresses { + if funding_address == address { + *step = WalletFundedScreenStep::FundsReceived; + self.funding_utxo = Some((*outpoint, tx_out.clone(), address.clone())) } } } @@ -902,38 +920,27 @@ impl ScreenLike for AddNewIdentityScreen { WalletFundedScreenStep::WaitingForAssetLock => { if let BackendTaskSuccessResult::CoreItem( CoreItem::ReceivedAvailableUTXOTransaction(tx, _), - ) = backend_task_success_result - { - if let Some(TransactionPayload::AssetLockPayloadType(asset_lock_payload)) = - tx.special_transaction_payload - { - if asset_lock_payload.credit_outputs.iter().any(|tx_out| { - let Ok(address) = Address::from_script( - &tx_out.script_pubkey, - self.app_context.network, - ) else { - return false; - }; - if let Some(wallet) = &self.selected_wallet { - let wallet = wallet.read().unwrap(); - wallet.known_addresses.contains_key(&address) - } else { - false - } - }) { - *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; + ) = &backend_task_success_result + && let Some(TransactionPayload::AssetLockPayloadType(asset_lock_payload)) = + &tx.special_transaction_payload + && asset_lock_payload.credit_outputs.iter().any(|tx_out| { + let Ok(address) = + Address::from_script(&tx_out.script_pubkey, self.app_context.network) + else { + return false; + }; + if let Some(wallet) = &self.selected_wallet { + let wallet = wallet.read().unwrap(); + wallet.known_addresses.contains_key(&address) + } else { + false } - } - } - } - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - if let BackendTaskSuccessResult::RegisteredIdentity(qualified_identity) = - backend_task_success_result + }) { - self.successful_qualified_identity_id = Some(qualified_identity.identity.id()); - *step = WalletFundedScreenStep::Success; + *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; } } + WalletFundedScreenStep::WaitingForPlatformAcceptance => {} WalletFundedScreenStep::Success => {} } } @@ -998,12 +1005,12 @@ impl ScreenLike for AddNewIdentityScreen { let wallet = wallet_guard.read().unwrap(); if wallet.identities.is_empty() { ui.heading(format!( - "{}. Choose an identity index. Leave this 0 if this is your first identity for this wallet.", + "{}. Choose an identity index for the wallet. Leaving this 0 is recommended.", step_number )); } else { ui.heading(format!( - "{}. Choose an identity index. Leaving this {} is recommended.", + "{}. Choose an identity index for the wallet. Leaving this {} is recommended.", step_number, self.next_identity_id(), )); diff --git a/src/ui/identities/funding_common.rs b/src/ui/identities/funding_common.rs index 6cce51edb..d1909e044 100644 --- a/src/ui/identities/funding_common.rs +++ b/src/ui/identities/funding_common.rs @@ -1,7 +1,13 @@ use arboard::Clipboard; use eframe::epaint::{Color32, ColorImage}; +use egui::Vec2; use image::Luma; use qrcode::QrCode; +use std::sync::{Arc, RwLock}; + +use crate::model::wallet::Wallet; +use dash_sdk::dashcore_rpc::dashcore::Address; +use dash_sdk::dpp::dashcore::{OutPoint, TxOut}; #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] pub enum WalletFundedScreenStep { @@ -33,7 +39,11 @@ pub fn generate_qr_code_image(pay_uri: &str) -> Result Result<(), String> { @@ -42,3 +52,43 @@ pub fn copy_to_clipboard(text: &str) -> Result<(), String> { .set_text(text.to_string()) .map_err(|e| e.to_string()) } + +pub fn capture_qr_funding_utxo_if_available( + step: &Arc>, + wallet: Option<&Arc>>, + funding_address: Option<&Address>, +) -> Option<(OutPoint, TxOut, Address)> { + if !matches!( + *step.read().expect("wallet funding step lock poisoned"), + WalletFundedScreenStep::WaitingOnFunds + ) { + return None; + } + + let address = funding_address.cloned()?; + + let wallet_arc = wallet?; + + let candidate_utxo = { + let wallet = wallet_arc + .read() + .expect("wallet lock poisoned while checking funding UTXO"); + wallet.utxos.get(&address).and_then(|utxos| { + utxos + .iter() + .filter(|(_, tx_out)| tx_out.value > 0) + .max_by_key(|(_, tx_out)| tx_out.value) + .map(|(outpoint, tx_out)| (*outpoint, tx_out.clone())) + }) + }; + + if let Some((outpoint, tx_out)) = candidate_utxo { + let mut step = step + .write() + .expect("wallet funding step write lock poisoned"); + *step = WalletFundedScreenStep::FundsReceived; + Some((outpoint, tx_out, address)) + } else { + None + } +} diff --git a/src/ui/identities/identities_screen.rs b/src/ui/identities/identities_screen.rs index d56e566e5..523f70fcb 100644 --- a/src/ui/identities/identities_screen.rs +++ b/src/ui/identities/identities_screen.rs @@ -123,10 +123,10 @@ impl IdentitiesScreen { if desired_idx >= lock.len() { break; } - if let Some(current_idx) = lock.get_index_of(&id) { - if current_idx != desired_idx { - lock.swap_indices(current_idx, desired_idx); - } + if let Some(current_idx) = lock.get_index_of(&id) + && current_idx != desired_idx + { + lock.swap_indices(current_idx, desired_idx); } } } @@ -197,17 +197,14 @@ impl IdentitiesScreen { } fn wallet_name_for(&self, qi: &QualifiedIdentity) -> String { - if let Some(master_identity_public_key) = qi.private_keys.find_master_key() { - if let Some(wallet_derivation_path) = + if let Some(master_identity_public_key) = qi.private_keys.find_master_key() + && let Some(wallet_derivation_path) = &master_identity_public_key.in_wallet_at_derivation_path - { - if let Some(alias) = self - .wallet_seed_hash_cache - .get(&wallet_derivation_path.wallet_seed_hash) - { - return alias.clone(); - } - } + && let Some(alias) = self + .wallet_seed_hash_cache + .get(&wallet_derivation_path.wallet_seed_hash) + { + return alias.clone(); } "".to_owned() } @@ -272,10 +269,10 @@ impl IdentitiesScreen { // Up/down reorder methods fn move_identity_up(&mut self, identity_id: &Identifier) { let mut lock = self.identities.lock().unwrap(); - if let Some(idx) = lock.get_index_of(identity_id) { - if idx > 0 { - lock.swap_indices(idx, idx - 1); - } + if let Some(idx) = lock.get_index_of(identity_id) + && idx > 0 + { + lock.swap_indices(idx, idx - 1); } drop(lock); self.save_current_order(); @@ -284,10 +281,10 @@ impl IdentitiesScreen { // arrow down fn move_identity_down(&mut self, identity_id: &Identifier) { let mut lock = self.identities.lock().unwrap(); - if let Some(idx) = lock.get_index_of(identity_id) { - if idx + 1 < lock.len() { - lock.swap_indices(idx, idx + 1); - } + if let Some(idx) = lock.get_index_of(identity_id) + && idx + 1 < lock.len() + { + lock.swap_indices(idx, idx + 1); } drop(lock); self.save_current_order(); @@ -308,10 +305,10 @@ impl IdentitiesScreen { // basically reorder the underlying IndexMap to match ephemeral_list for (desired_idx, qi) in ephemeral_list.into_iter().enumerate() { let id = qi.identity.id(); - if let Some(current_idx) = lock.get_index_of(&id) { - if current_idx != desired_idx { - lock.swap_indices(current_idx, desired_idx); - } + if let Some(current_idx) = lock.get_index_of(&id) + && current_idx != desired_idx + { + lock.swap_indices(current_idx, desired_idx); } } } @@ -624,50 +621,38 @@ impl IdentitiesScreen { let actions_response = ui.add(actions_button).on_hover_text("Manage identity credits"); let actions_popup_id = ui.make_persistent_id(format!("actions_popup_{}", qualified_identity.identity.id().to_string(Encoding::Base58))); - - if actions_response.clicked() { - ui.memory_mut(|mem| mem.toggle_popup(actions_popup_id)); - } - - egui::popup::popup_below_widget( - ui, - actions_popup_id, - &actions_response, - egui::PopupCloseBehavior::CloseOnClickOutside, - |ui| { - ui.set_min_width(150.0); - - if ui.button("💸 Withdraw").on_hover_text("Withdraw credits from this identity to a Dash Core address").clicked() { - action = AppAction::AddScreen( - Screen::WithdrawalScreen(WithdrawalScreen::new( - qualified_identity.clone(), - &self.app_context, - )), - ); - ui.close_menu(); - } - - if ui.button("💰 Top up").on_hover_text("Increase this identity's balance by sending it Dash from the Core chain").clicked() { - action = AppAction::AddScreen( - Screen::TopUpIdentityScreen(TopUpIdentityScreen::new( - qualified_identity.clone(), - &self.app_context, - )), - ); - ui.close_menu(); - } - - if ui.button("📤 Transfer").on_hover_text("Transfer credits from this identity to another identity").clicked() { - action = AppAction::AddScreen( - Screen::TransferScreen(TransferScreen::new( - qualified_identity.clone(), - &self.app_context, - )), - ); - ui.close_menu(); - } - }, - ); + egui::Popup::from_toggle_button_response(&actions_response).id(actions_popup_id) + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) + .show(|ui| { + ui.set_min_width(150.0); + + if ui.add_sized([ui.available_width(), 0.0], egui::Button::new("💸 Withdraw")).on_hover_text("Withdraw credits from this identity to a Dash Core address").clicked() { + action = AppAction::AddScreen( + Screen::WithdrawalScreen(WithdrawalScreen::new( + qualified_identity.clone(), + &self.app_context, + )), + ); + } + + if ui.add_sized([ui.available_width(), 0.0], egui::Button::new("💰 Top up")).on_hover_text("Increase this identity's balance by sending it Dash from the Core chain").clicked() { + action = AppAction::AddScreen( + Screen::TopUpIdentityScreen(TopUpIdentityScreen::new( + qualified_identity.clone(), + &self.app_context, + )), + ); + } + + if ui.add_sized([ui.available_width(), 0.0], egui::Button::new("📤 Transfer")).on_hover_text("Transfer credits from this identity to another identity").clicked() { + action = AppAction::AddScreen( + Screen::TransferScreen(TransferScreen::new( + qualified_identity.clone(), + &self.app_context, + )), + ); + } + }); }); }); }); @@ -690,20 +675,12 @@ impl IdentitiesScreen { .corner_radius(3.0) .min_size(egui::vec2(50.0, 20.0)); - let response = ui.add(button).on_hover_text("View and manage keys for this identity"); + let button_response = ui.add(button).on_hover_text("View and manage keys for this identity"); let popup_id = ui.make_persistent_id(format!("keys_popup_{}", qualified_identity.identity.id().to_string(Encoding::Base58))); - - if response.clicked() { - ui.memory_mut(|mem| mem.toggle_popup(popup_id)); - } - - egui::popup::popup_below_widget( - ui, - popup_id, - &response, - egui::PopupCloseBehavior::CloseOnClickOutside, - |ui| { + egui::Popup::from_toggle_button_response(&button_response).id(popup_id) + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) + .show(|ui| { ui.set_min_width(200.0); // Main Identity Keys @@ -743,7 +720,7 @@ impl IdentitiesScreen { holding_private_key, &self.app_context, ))); - ui.close_menu(); + ui.close_kind(egui::UiKind::Menu); } } } @@ -790,7 +767,7 @@ impl IdentitiesScreen { holding_private_key, &self.app_context, ))); - ui.close_menu(); + ui.close_kind(egui::UiKind::Menu); } } } @@ -809,7 +786,7 @@ impl IdentitiesScreen { qualified_identity.clone(), &self.app_context, ))); - ui.close_menu(); + ui.close_kind(egui::UiKind::Menu); } } }, @@ -856,8 +833,9 @@ impl IdentitiesScreen { action } - fn show_identity_to_remove(&mut self, ctx: &Context) { + fn show_identity_to_remove(&mut self, ctx: &Context) -> AppAction { if let Some(identity_to_remove) = self.identity_to_remove.clone() { + let action = AppAction::None; egui::Window::new("Confirm Removal") .collapsible(false) .resizable(false) @@ -904,6 +882,9 @@ impl IdentitiesScreen { } }); }); + action + } else { + AppAction::None } } @@ -1024,6 +1005,11 @@ impl ScreenLike for IdentitiesScreen { inner_action |= self.render_identities_view(ui, &identities_vec); } + // Handle identity removal confirmation dialog + if self.identity_to_remove.is_some() { + inner_action |= self.show_identity_to_remove(ctx); + } + // Show either refreshing indicator or message, but not both if let IdentitiesRefreshingStatus::Refreshing(start_time) = self.refreshing_status { ui.add_space(25.0); // Space above @@ -1060,10 +1046,6 @@ impl ScreenLike for IdentitiesScreen { inner_action }); - if self.identity_to_remove.is_some() { - self.show_identity_to_remove(ctx); - } - match action { AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::RefreshIdentity(_))) => { self.refreshing_status = diff --git a/src/ui/identities/keys/key_info_screen.rs b/src/ui/identities/keys/key_info_screen.rs index 4a7166ced..1c12968d8 100644 --- a/src/ui/identities/keys/key_info_screen.rs +++ b/src/ui/identities/keys/key_info_screen.rs @@ -26,7 +26,7 @@ use dash_sdk::dpp::identity::identity_public_key::contract_bounds::ContractBound use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::IdentityPublicKey; use eframe::egui::{self, Context}; -use egui::{Color32, RichText, ScrollArea, TextEdit}; +use egui::{Color32, RichText, ScrollArea}; use std::sync::{Arc, RwLock}; pub struct KeyInfoScreen { @@ -299,11 +299,22 @@ impl ScreenLike for KeyInfoScreen { match private_key { PrivateKeyData::Clear(clear) | PrivateKeyData::AlwaysClear(clear) => { - let private_key_hex = hex::encode(clear); - ui.add( - TextEdit::singleline(&mut private_key_hex.as_str().to_owned()) - .desired_width(f32::INFINITY), - ); + egui::Grid::new("private_key_grid") + .num_columns(2) + .spacing([10.0, 10.0]) + .show(ui, |ui| { + ui.label( + RichText::new("Private Key (Hex):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_hex = hex::encode(clear); + ui.label( + RichText::new(private_key_hex) + .color(ui.visuals().text_color()), + ); + ui.end_row(); + }); ui.add_space(10.0); if ui.button("Remove private key from DET").clicked() { self.show_confirm_remove_private_key = true; @@ -322,27 +333,74 @@ impl ScreenLike for KeyInfoScreen { && self.selected_wallet.is_some() { if let Some(private_key) = self.decrypted_private_key { - let private_key_wif = private_key.to_wif(); - ui.add( - TextEdit::multiline( - &mut private_key_wif.as_str().to_owned(), - ) - .desired_width(f32::INFINITY), - ); + egui::Grid::new("private_key_grid_wallet") + .num_columns(2) + .spacing([10.0, 10.0]) + .show(ui, |ui| { + ui.label( + RichText::new("Private Key (WIF):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_wif = private_key.to_wif(); + ui.label( + RichText::new(private_key_wif) + .color(ui.visuals().text_color()), + ); + ui.end_row(); + + ui.label( + RichText::new("Private Key (Hex):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_hex = + hex::encode(private_key.inner.secret_bytes()); + ui.label( + RichText::new(private_key_hex) + .color(ui.visuals().text_color()), + ); + ui.end_row(); + }); } else { let wallet = self.selected_wallet.as_ref().unwrap().read().unwrap(); match wallet.private_key_at_derivation_path( &derivation_path.derivation_path, + self.app_context.network, ) { Ok(private_key) => { - let private_key_wif = private_key.to_wif(); - ui.add( - TextEdit::multiline( - &mut private_key_wif.as_str().to_owned(), - ) - .desired_width(f32::INFINITY), - ); + egui::Grid::new("private_key_grid_wallet2") + .num_columns(2) + .spacing([10.0, 10.0]) + .show(ui, |ui| { + ui.label( + RichText::new("Private Key (WIF):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_wif = private_key.to_wif(); + ui.label( + RichText::new(private_key_wif) + .color(ui.visuals().text_color()), + ); + ui.end_row(); + + ui.label( + RichText::new("Private Key (Hex):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_hex = hex::encode( + private_key.inner.secret_bytes(), + ); + ui.label( + RichText::new(private_key_hex) + .color(ui.visuals().text_color()), + ); + ui.end_row(); + }); + self.decrypted_private_key = Some(private_key); } Err(e) => { @@ -365,15 +423,40 @@ impl ScreenLike for KeyInfoScreen { self.selected_wallet.as_ref().unwrap().read().unwrap(); match wallet.private_key_at_derivation_path( &derivation_path.derivation_path, + self.app_context.network, ) { Ok(private_key) => { - let private_key_wif = private_key.to_wif(); - ui.add( - TextEdit::multiline( - &mut private_key_wif.as_str().to_owned(), - ) - .desired_width(f32::INFINITY), - ); + egui::Grid::new("private_key_grid_wallet2") + .num_columns(2) + .spacing([10.0, 10.0]) + .show(ui, |ui| { + ui.label( + RichText::new("Private Key (WIF):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_wif = private_key.to_wif(); + ui.label( + RichText::new(private_key_wif) + .color(ui.visuals().text_color()), + ); + ui.end_row(); + + ui.label( + RichText::new("Private Key (Hex):") + .strong() + .color(ui.visuals().text_color()), + ); + let private_key_hex = hex::encode( + private_key.inner.secret_bytes(), + ); + ui.label( + RichText::new(private_key_hex) + .color(ui.visuals().text_color()), + ); + ui.end_row(); + }); + self.decrypted_private_key = Some(private_key); } Err(e) => { diff --git a/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs b/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs index ccb420274..4b9ea0a4c 100644 --- a/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs +++ b/src/ui/identities/top_up_identity_screen/by_using_unused_asset_lock.rs @@ -115,8 +115,6 @@ impl TopUpIdentityScreen { ui.vertical_centered(|ui| match step { WalletFundedScreenStep::WaitingForPlatformAcceptance => { ui.heading("=> Waiting for Platform acknowledgement <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); } WalletFundedScreenStep::Success => { ui.heading("...Success..."); diff --git a/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs b/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs index 762f4a051..0a9e598e7 100644 --- a/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs +++ b/src/ui/identities/top_up_identity_screen/by_using_unused_balance.rs @@ -68,14 +68,12 @@ impl TopUpIdentityScreen { ui.vertical_centered(|ui| { match step { WalletFundedScreenStep::WaitingForAssetLock => { - ui.heading("=> Waiting for Core Chain to produce proof of transfer of funds. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); + ui.heading( + "=> Waiting for Core Chain to produce proof of transfer of funds. <=", + ); } WalletFundedScreenStep::WaitingForPlatformAcceptance => { ui.heading("=> Waiting for Platform acknowledgement <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); } WalletFundedScreenStep::Success => { ui.heading("...Success..."); diff --git a/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs b/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs index ab7152fd7..93739e8b2 100644 --- a/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs +++ b/src/ui/identities/top_up_identity_screen/by_wallet_qr_code.rs @@ -1,7 +1,7 @@ use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::identity::{IdentityTask, IdentityTopUpInfo, TopUpIdentityFundingMethod}; -use crate::ui::identities::funding_common::{copy_to_clipboard, generate_qr_code_image}; +use crate::ui::identities::funding_common::{self, copy_to_clipboard, generate_qr_code_image}; use crate::ui::identities::top_up_identity_screen::{TopUpIdentityScreen, WalletFundedScreenStep}; use dash_sdk::dashcore_rpc::RpcApi; use eframe::epaint::TextureHandle; @@ -10,11 +10,9 @@ use std::sync::Arc; impl TopUpIdentityScreen { fn render_qr_code(&mut self, ui: &mut egui::Ui, amount: f64) -> Result<(), String> { - let (address, _should_check_balance) = { - // Scope the write lock to ensure it's dropped before calling `start_balance_check`. - + let address = { if let Some(wallet_guard) = self.wallet.as_ref() { - // Get the receive address + // Get the receive address from the selected wallet if self.funding_address.is_none() { let mut wallet = wallet_guard.write().unwrap(); let receive_address = wallet.receive_address( @@ -23,49 +21,33 @@ impl TopUpIdentityScreen { Some(&self.app_context), )?; - if let Some(has_address) = self.core_has_funding_address { - if !has_address { - self.app_context - .core_client - .read() - .expect("Core client lock was poisoned") - .import_address( - &receive_address, - Some("Managed by Dash Evo Tool"), - Some(false), - ) - .map_err(|e| e.to_string())?; - } - self.funding_address = Some(receive_address); - } else { - let info = self - .app_context - .core_client - .read() - .expect("Core client lock was poisoned") - .get_address_info(&receive_address) + // Import address to Core if needed for monitoring + let core_client = self + .app_context + .core_client + .read() + .map_err(|_| "Core client lock was poisoned".to_string())?; + + let info = core_client + .get_address_info(&receive_address) + .map_err(|e| e.to_string())?; + + if !(info.is_watchonly || info.is_mine) { + core_client + .import_address( + &receive_address, + Some("Managed by Dash Evo Tool"), + Some(false), + ) .map_err(|e| e.to_string())?; - - if !(info.is_watchonly || info.is_mine) { - self.app_context - .core_client - .read() - .expect("Core client lock was poisoned") - .import_address( - &receive_address, - Some("Managed by Dash Evo Tool"), - Some(false), - ) - .map_err(|e| e.to_string())?; - } - self.funding_address = Some(receive_address); - self.core_has_funding_address = Some(true); } - // Extract the address to return it outside this scope - (self.funding_address.as_ref().unwrap().clone(), true) + drop(core_client); + + self.funding_address = Some(receive_address.clone()); + receive_address } else { - (self.funding_address.as_ref().unwrap().clone(), false) + self.funding_address.as_ref().unwrap().clone() } } else { return Err("No wallet selected".to_string()); @@ -109,6 +91,15 @@ impl TopUpIdentityScreen { } pub fn render_ui_by_wallet_qr_code(&mut self, ui: &mut Ui, step_number: u32) -> AppAction { + // Update state when the QR funding address receives funds + if let Some(utxo) = funding_common::capture_qr_funding_utxo_if_available( + &self.step, + self.wallet.as_ref(), + self.funding_address.as_ref(), + ) { + self.funding_utxo = Some(utxo); + } + // Extract the step from the RwLock to minimize borrow scope let step = *self.step.read().unwrap(); @@ -124,19 +115,23 @@ impl TopUpIdentityScreen { self.top_up_funding_amount_input(ui); - let Ok(amount_dash) = self.funding_amount.parse::() else { - return AppAction::None; - }; - - if amount_dash <= 0.0 { - return AppAction::None; + if step == WalletFundedScreenStep::WaitingOnFunds { + ui.ctx() + .request_repaint_after(std::time::Duration::from_secs(1)); } - let response = ui.with_layout( - egui::Layout::top_down(egui::Align::Min).with_cross_align(egui::Align::Center), - |ui| { - if let Err(e) = self.render_qr_code(ui, amount_dash) { - self.error_message = Some(e); + let response = ui.vertical_centered(|ui| { + // Only try to render QR code if we have a valid amount + if let Ok(amount_dash) = self.funding_amount.parse::() { + if amount_dash > 0.0 { + if let Err(e) = self.render_qr_code(ui, amount_dash) { + self.error_message = Some(e); + } + } else { + ui.label("Please enter an amount greater than 0"); + } + } else if !self.funding_amount.is_empty() { + ui.label("Please enter a valid amount"); } ui.add_space(20.0); @@ -162,7 +157,7 @@ impl TopUpIdentityScreen { .unwrap_or_default(); let identity_input = IdentityTopUpInfo { qualified_identity: self.identity.clone(), - wallet: Arc::clone(selected_wallet), // Clone the Arc reference + wallet: Arc::clone(selected_wallet), identity_funding_method: TopUpIdentityFundingMethod::FundWithUtxo( utxo, tx_out, @@ -175,7 +170,6 @@ impl TopUpIdentityScreen { let mut step = self.step.write().unwrap(); *step = WalletFundedScreenStep::WaitingForAssetLock; - // Create the backend task to register the identity return AppAction::BackendTask(BackendTask::IdentityTask( IdentityTask::TopUpIdentity(identity_input), )); @@ -186,13 +180,9 @@ impl TopUpIdentityScreen { ui.heading( "=> Waiting for Core Chain to produce proof of transfer of funds. <=", ); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); } WalletFundedScreenStep::WaitingForPlatformAcceptance => { ui.heading("=> Waiting for Platform acknowledgement. <="); - ui.add_space(20.0); - ui.label("NOTE: If this gets stuck, the funds were likely either transferred to the wallet or asset locked,\nand you can use the funding method selector in step 1 to change the method and use those funds to complete the process."); } WalletFundedScreenStep::Success => { ui.heading("...Success..."); diff --git a/src/ui/identities/top_up_identity_screen/mod.rs b/src/ui/identities/top_up_identity_screen/mod.rs index d078545ca..96bdd6011 100644 --- a/src/ui/identities/top_up_identity_screen/mod.rs +++ b/src/ui/identities/top_up_identity_screen/mod.rs @@ -29,12 +29,14 @@ use egui::{Color32, ComboBox, ScrollArea, Ui}; use std::sync::atomic::Ordering; use std::sync::{Arc, RwLock}; +const WALLET_SELECTION_TOOLTIP: &str = "This wallet will provide the address for receiving funds \ +and create the asset lock transaction to top up your identity."; + pub struct TopUpIdentityScreen { pub identity: QualifiedIdentity, step: Arc>, funding_asset_lock: Option<(Transaction, AssetLockProof, Address)>, wallet: Option>>, - core_has_funding_address: Option, funding_address: Option
, funding_method: Arc>, funding_amount: String, @@ -55,7 +57,6 @@ impl TopUpIdentityScreen { step: Arc::new(RwLock::new(WalletFundedScreenStep::ChooseFundingMethod)), funding_asset_lock: None, wallet: None, - core_has_funding_address: None, funding_address: None, funding_method: Arc::new(RwLock::new(FundingMethod::NoSelection)), funding_amount: "".to_string(), @@ -71,10 +72,15 @@ impl TopUpIdentityScreen { } fn render_wallet_selection(&mut self, ui: &mut Ui) -> bool { - if self.app_context.has_wallet.load(Ordering::Relaxed) { - let wallets = self.app_context.wallets.read().unwrap(); + let mut selected_wallet_update: Option>> = None; + let mut step_update_method: Option = None; + + let rendered = if self.app_context.has_wallet.load(Ordering::Relaxed) { + let wallets_guard = self.app_context.wallets.read().unwrap(); + let wallets = &*wallets_guard; + if wallets.len() > 1 { - // Get the current funding method + // Cache current funding method to avoid holding the lock across UI callbacks let funding_method = *self.funding_method.read().unwrap(); // Retrieve the alias of the currently selected wallet, if any @@ -114,8 +120,8 @@ impl TopUpIdentityScreen { ui.add_enabled_ui(has_required_resources, |ui| { if ui.selectable_label(is_selected, wallet_alias).clicked() { - // Update the selected wallet from app_context - self.wallet = Some(wallet.clone()); + selected_wallet_update = Some(wallet.clone()); + step_update_method = Some(funding_method); } }); } @@ -123,7 +129,7 @@ impl TopUpIdentityScreen { true } else if let Some(wallet) = wallets.values().next() { if self.wallet.is_none() { - // Get the current funding method + // Cache current funding method to avoid holding the lock across updates let funding_method = *self.funding_method.read().unwrap(); // Check if the wallet has the required resources @@ -140,7 +146,8 @@ impl TopUpIdentityScreen { if has_required_resources { // Automatically select the only available wallet from app_context - self.wallet = Some(wallet.clone()); + selected_wallet_update = Some(wallet.clone()); + step_update_method = Some(funding_method); } } ui.label(format!( @@ -160,7 +167,36 @@ impl TopUpIdentityScreen { "No wallets available. Please create or import a wallet first.", ); false + }; + + if let Some(wallet) = selected_wallet_update { + self.wallet = Some(wallet); + self.funding_address = None; + self.funding_asset_lock = None; + self.funding_utxo = None; + self.copied_to_clipboard = None; + + if let Some(method) = step_update_method { + self.update_step_after_wallet_change(method); + } else { + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::ChooseFundingMethod; + } } + + rendered + } + + /// Adjust the current step to match the funding method after a wallet switch. + fn update_step_after_wallet_change(&mut self, funding_method: FundingMethod) { + let mut step = self.step.write().unwrap(); + *step = match funding_method { + FundingMethod::AddressWithQRCode => WalletFundedScreenStep::WaitingOnFunds, + FundingMethod::UseUnusedAssetLock | FundingMethod::UseWalletBalance => { + WalletFundedScreenStep::ReadyToCreate + } + FundingMethod::NoSelection => WalletFundedScreenStep::ChooseFundingMethod, + }; } fn render_funding_method(&mut self, ui: &mut egui::Ui) { @@ -360,20 +396,36 @@ impl ScreenLike for TopUpIdentityScreen { } } fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + if let BackendTaskSuccessResult::ToppedUpIdentity(qualified_identity) = + &backend_task_success_result + { + self.identity = qualified_identity.clone(); + self.funding_address = None; + self.funding_utxo = None; + self.funding_amount.clear(); + self.funding_amount_exact = None; + self.copied_to_clipboard = None; + self.error_message = None; + + let mut step = self.step.write().unwrap(); + *step = WalletFundedScreenStep::Success; + return; + } + let mut step = self.step.write().unwrap(); - match *step { + let current_step = *step; + match current_step { WalletFundedScreenStep::ChooseFundingMethod => {} WalletFundedScreenStep::WaitingOnFunds => { - if let Some(funding_address) = self.funding_address.as_ref() { - if let BackendTaskSuccessResult::CoreItem( + if let Some(funding_address) = self.funding_address.as_ref() + && let BackendTaskSuccessResult::CoreItem( CoreItem::ReceivedAvailableUTXOTransaction(_, outpoints_with_addresses), - ) = backend_task_success_result - { - for (outpoint, tx_out, address) in outpoints_with_addresses { - if funding_address == &address { - *step = WalletFundedScreenStep::FundsReceived; - self.funding_utxo = Some((outpoint, tx_out, address)) - } + ) = &backend_task_success_result + { + for (outpoint, tx_out, address) in outpoints_with_addresses { + if funding_address == address { + *step = WalletFundedScreenStep::FundsReceived; + self.funding_utxo = Some((*outpoint, tx_out.clone(), address.clone())) } } } @@ -383,37 +435,27 @@ impl ScreenLike for TopUpIdentityScreen { WalletFundedScreenStep::WaitingForAssetLock => { if let BackendTaskSuccessResult::CoreItem( CoreItem::ReceivedAvailableUTXOTransaction(tx, _), - ) = backend_task_success_result - { - if let Some(TransactionPayload::AssetLockPayloadType(asset_lock_payload)) = - tx.special_transaction_payload - { - if asset_lock_payload.credit_outputs.iter().any(|tx_out| { - let Ok(address) = Address::from_script( - &tx_out.script_pubkey, - self.app_context.network, - ) else { - return false; - }; - if let Some(wallet) = &self.wallet { - let wallet = wallet.read().unwrap(); - wallet.known_addresses.contains_key(&address) - } else { - false - } - }) { - *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; + ) = &backend_task_success_result + && let Some(TransactionPayload::AssetLockPayloadType(asset_lock_payload)) = + &tx.special_transaction_payload + && asset_lock_payload.credit_outputs.iter().any(|tx_out| { + let Ok(address) = + Address::from_script(&tx_out.script_pubkey, self.app_context.network) + else { + return false; + }; + if let Some(wallet) = &self.wallet { + let wallet = wallet.read().unwrap(); + wallet.known_addresses.contains_key(&address) + } else { + false } - } - } - } - WalletFundedScreenStep::WaitingForPlatformAcceptance => { - if let BackendTaskSuccessResult::ToppedUpIdentity(_qualified_identity) = - backend_task_success_result + }) { - *step = WalletFundedScreenStep::Success; + *step = WalletFundedScreenStep::WaitingForPlatformAcceptance; } } + WalletFundedScreenStep::WaitingForPlatformAcceptance => {} WalletFundedScreenStep::Success => {} } } @@ -493,10 +535,16 @@ impl ScreenLike for TopUpIdentityScreen { || funding_method == FundingMethod::UseUnusedAssetLock || funding_method == FundingMethod::AddressWithQRCode { - ui.heading(format!( - "{}. Choose the wallet to use to top up this identity.", - step_number - )); + ui.horizontal(|ui| { + ui.heading(format!( + "{}. Choose the wallet to use to top up this identity.", + step_number + )); + ui.add_space(10.0); + + // Add info icon with hover tooltip + crate::ui::helpers::info_icon_button(ui, WALLET_SELECTION_TOOLTIP); + }); step_number += 1; ui.add_space(10.0); diff --git a/src/ui/identities/transfer_screen.rs b/src/ui/identities/transfer_screen.rs index 15fb5beaa..5596766e2 100644 --- a/src/ui/identities/transfer_screen.rs +++ b/src/ui/identities/transfer_screen.rs @@ -2,8 +2,13 @@ use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::identity::IdentityTask; use crate::context::AppContext; +use crate::model::amount::Amount; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::component_trait::{Component, ComponentResponse}; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::top_panel::add_top_panel; @@ -38,13 +43,16 @@ pub enum TransferCreditsStatus { pub struct TransferScreen { pub identity: QualifiedIdentity, selected_key: Option, + known_identities: Vec, receiver_identity_id: String, - amount: String, + amount: Option, + amount_input: Option, transfer_credits_status: TransferCreditsStatus, error_message: Option, max_amount: u64, pub app_context: Arc, confirmation_popup: bool, + confirmation_dialog: Option, selected_wallet: Option>>, wallet_password: String, show_password: bool, @@ -52,6 +60,10 @@ pub struct TransferScreen { impl TransferScreen { pub fn new(identity: QualifiedIdentity, app_context: &Arc) -> Self { + let known_identities = app_context + .load_local_qualified_identities() + .expect("Identities not loaded"); + let max_amount = identity.identity.balance(); let identity_clone = identity.identity.clone(); let selected_key = identity_clone.get_first_public_key_matching( @@ -66,13 +78,16 @@ impl TransferScreen { Self { identity, selected_key: selected_key.cloned(), + known_identities, receiver_identity_id: String::new(), - amount: String::new(), + amount: Some(Amount::new_dash(0.0)), + amount_input: None, transfer_credits_status: TransferCreditsStatus::NotStarted, error_message: None, max_amount, app_context: app_context.clone(), confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -92,110 +107,153 @@ impl TransferScreen { } fn render_amount_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Amount in Dash:"); - - ui.text_edit_singleline(&mut self.amount); + // Show available balance + let balance_in_dash = self.max_amount as f64 / 100_000_000_000.0; + ui.label(format!("Available balance: {:.8} DASH", balance_in_dash)); + ui.add_space(5.0); + + // Calculate max amount minus fee for the "Max" button + let max_amount_minus_fee = (self.max_amount as f64 / 100_000_000_000.0 - 0.0001).max(0.0); + let max_amount_credits = (max_amount_minus_fee * 100_000_000_000.0) as u64; + + let amount_input = self.amount_input.get_or_insert_with(|| { + AmountInput::new(Amount::new_dash(0.0)) + .with_label("Amount:") + .with_max_button(true) + .with_max_amount(Some(max_amount_credits)) + }); - if ui.button("Max").clicked() { - let amount_in_dash = self.max_amount as f64 / 100_000_000_000.0 - 0.0001; // Subtract a small amount to cover gas fee which is usually around 0.00002 Dash - self.amount = format!("{:.8}", amount_in_dash); + // Check if input should be disabled when operation is in progress + let enabled = match self.transfer_credits_status { + TransferCreditsStatus::WaitingForResult(_) | TransferCreditsStatus::Complete => false, + TransferCreditsStatus::NotStarted | TransferCreditsStatus::ErrorMessage(_) => { + amount_input.set_max_amount(Some(max_amount_credits)); + true } - }); + }; + + let response = ui.add_enabled_ui(enabled, |ui| amount_input.show(ui)).inner; + + response.inner.update(&mut self.amount); + // errors are handled inside AmountInput } fn render_to_identity_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Receiver Identity Id:"); + ui.add( + IdentitySelector::new( + "transfer_recipient_selector", + &mut self.receiver_identity_id, + &self.known_identities, + ) + .width(300.0) + .label("Receiver Identity ID:") + .exclude(&[self.identity.identity.id()]), + ); + } - ui.text_edit_singleline(&mut self.receiver_identity_id); - }); + /// Handle the confirmation action when user clicks OK + fn confirmation_ok(&mut self) -> AppAction { + self.confirmation_popup = false; + self.confirmation_dialog = None; // Reset the dialog for next use + + // Validate identifier + let identifier = match self.validate_receiver_identifier() { + Ok(id) => id, + Err(error) => { + self.set_error_state(error); + return AppAction::None; + } + }; + + // Validate selected key + let selected_key = match self.selected_key.as_ref() { + Some(key) => key, + None => { + self.set_error_state("No selected key".to_string()); + return AppAction::None; + } + }; + + // Use the amount directly since it's already an Amount struct + let credits = self.amount.as_ref().map(|v| v.value()).unwrap_or_default() as u128; + if credits == 0 { + self.error_message = Some("Amount must be greater than 0".to_string()); + self.transfer_credits_status = + TransferCreditsStatus::ErrorMessage("Amount must be greater than 0".to_string()); + self.confirmation_popup = false; + return AppAction::None; + } + + // Set waiting state and create backend task + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.transfer_credits_status = TransferCreditsStatus::WaitingForResult(now); + + AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::Transfer( + self.identity.clone(), + identifier, + credits as Credits, + Some(selected_key.id()), + ))) + } + + /// Handle the cancel action when user clicks Cancel or closes dialog + fn confirmation_cancel(&mut self) -> AppAction { + self.confirmation_popup = false; + self.confirmation_dialog = None; // Reset the dialog for next use + AppAction::None + } + + /// Validate the receiver identity identifier + fn validate_receiver_identifier(&self) -> Result { + if self.receiver_identity_id.is_empty() { + return Err("Invalid identifier".to_string()); + } + + Identifier::from_string_try_encodings( + &self.receiver_identity_id, + &[Encoding::Base58, Encoding::Hex], + ) + .map_err(|_| "Invalid identifier".to_string()) + } + + /// Set error state with the given message + fn set_error_state(&mut self, error: String) { + self.error_message = Some(error.clone()); + self.transfer_credits_status = TransferCreditsStatus::ErrorMessage(error); } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut app_action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Transfer") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - let identifier = if self.receiver_identity_id.is_empty() { - self.error_message = Some("Invalid identifier".to_string()); - self.transfer_credits_status = - TransferCreditsStatus::ErrorMessage("Invalid identifier".to_string()); - self.confirmation_popup = false; - return; - } else { - match Identifier::from_string_try_encodings( - &self.receiver_identity_id, - &[Encoding::Base58, Encoding::Hex], - ) { - Ok(identifier) => identifier, - Err(_) => { - self.error_message = Some("Invalid identifier".to_string()); - self.transfer_credits_status = TransferCreditsStatus::ErrorMessage( - "Invalid identifier".to_string(), - ); - self.confirmation_popup = false; - return; - } - } - }; - - let Some(selected_key) = self.selected_key.as_ref() else { - self.error_message = Some("No selected key".to_string()); - self.transfer_credits_status = - TransferCreditsStatus::ErrorMessage("No selected key".to_string()); - self.confirmation_popup = false; - return; - }; - - ui.label(format!( - "Are you sure you want to transfer {} Dash to {}", - self.amount, self.receiver_identity_id - )); - let parts: Vec<&str> = self.amount.split('.').collect(); - let mut credits: u128 = 0; - - // Process the whole number part if it exists. - if let Some(whole) = parts.first() { - if let Ok(whole_number) = whole.parse::() { - credits += whole_number * 100_000_000_000; // Whole Dash amount to credits - } - } + // Prepare values before borrowing + let Some(amount) = &self.amount else { + self.set_error_state("Incorrect or empty amount".to_string()); + return AppAction::None; + }; - // Process the fractional part if it exists. - if let Some(fraction) = parts.get(1) { - let fraction_length = fraction.len(); - let fraction_number = fraction.parse::().unwrap_or(0); - // Calculate the multiplier based on the number of digits in the fraction. - let multiplier = 10u128.pow(11 - fraction_length as u32); - credits += fraction_number * multiplier; // Fractional Dash to credits - } + let receiver_id = self.receiver_identity_id.clone(); - if ui.button("Confirm").clicked() { - self.confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.transfer_credits_status = TransferCreditsStatus::WaitingForResult(now); - app_action = - AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::Transfer( - self.identity.clone(), - identifier, - credits as Credits, - Some(selected_key.id()), - ))); - } - if ui.button("Cancel").clicked() { - self.confirmation_popup = false; - } - }); - if !is_open { - self.confirmation_popup = false; + let msg = format!( + "Are you sure you want to transfer {} to {}?", + amount, receiver_id + ); + + // Lazy initialization of the confirmation dialog + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Transfer", msg) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + }); + + let response = confirmation_dialog.show(ui); + + // Handle the response using the Component pattern + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => self.confirmation_ok(), + Some(ConfirmationStatus::Canceled) => self.confirmation_cancel(), + None => AppAction::None, } - app_action } pub fn show_success(&self, ui: &mut Ui) -> AppAction { @@ -369,6 +427,13 @@ impl ScreenLike for TransferScreen { ui.add_space(10.0); // Transfer button + let ready = self.amount.is_some() + && !self.receiver_identity_id.is_empty() + && self.selected_key.is_some() + && !matches!( + self.transfer_credits_status, + TransferCreditsStatus::WaitingForResult(_), + ); let mut new_style = (**ui.style()).clone(); new_style.spacing.button_padding = egui::vec2(10.0, 5.0); ui.set_style(new_style); @@ -376,7 +441,11 @@ impl ScreenLike for TransferScreen { .fill(Color32::from_rgb(0, 128, 255)) .frame(true) .corner_radius(3.0); - if ui.add(button).clicked() { + if ui + .add_enabled(ready, button) + .on_disabled_hover_text("Please ensure all fields are filled correctly") + .clicked() + { self.confirmation_popup = true; } diff --git a/src/ui/identities/withdraw_screen.rs b/src/ui/identities/withdraw_screen.rs index 88acc78a3..0be9a7f0d 100644 --- a/src/ui/identities/withdraw_screen.rs +++ b/src/ui/identities/withdraw_screen.rs @@ -2,13 +2,17 @@ use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::identity::IdentityTask; use crate::context::AppContext; +use crate::model::amount::Amount; use crate::model::qualified_identity::encrypted_key_storage::PrivateKeyData; use crate::model::qualified_identity::{IdentityType, PrivateKeyTarget, QualifiedIdentity}; use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::components::{Component, ComponentResponse}; use crate::ui::helpers::{TransactionType, add_identity_key_chooser}; use crate::ui::{MessageType, Screen, ScreenLike}; use dash_sdk::dashcore_rpc::dashcore::Address; @@ -41,10 +45,11 @@ pub struct WithdrawalScreen { pub identity: QualifiedIdentity, selected_key: Option, withdrawal_address: String, - withdrawal_amount: String, + withdrawal_amount: Option, + withdrawal_amount_input: Option, max_amount: u64, pub app_context: Arc, - confirmation_popup: bool, + confirmation_dialog: Option, withdraw_from_identity_status: WithdrawFromIdentityStatus, selected_wallet: Option>>, wallet_password: String, @@ -69,10 +74,11 @@ impl WithdrawalScreen { identity, selected_key: selected_key.cloned(), withdrawal_address: String::new(), - withdrawal_amount: String::new(), + withdrawal_amount: None, + withdrawal_amount_input: None, max_amount, app_context: app_context.clone(), - confirmation_popup: false, + confirmation_dialog: None, withdraw_from_identity_status: WithdrawFromIdentityStatus::NotStarted, selected_wallet, wallet_password: String::new(), @@ -94,21 +100,31 @@ impl WithdrawalScreen { } fn render_amount_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Amount (dash):"); - - ui.text_edit_singleline(&mut self.withdrawal_amount); + let max_amount_minus_fee = (self.max_amount as f64 / 100_000_000_000.0 - 0.0001).max(0.0); + let max_amount_credits = (max_amount_minus_fee * 100_000_000_000.0) as u64; + + // Lazy initialization with basic configuration + let amount_input = self.withdrawal_amount_input.get_or_insert_with(|| { + AmountInput::new(Amount::new_dash(0.0)) + .with_label("Amount:") + .with_max_button(true) + }); - if ui.button("Max").clicked() { - let expected_max_amount = self.max_amount.saturating_sub(500000000) as f64 * 1e-11; + // Check if input should be disabled when operation is in progress + let enabled = match self.withdraw_from_identity_status { + WithdrawFromIdentityStatus::WaitingForResult(_) + | WithdrawFromIdentityStatus::Complete => false, + WithdrawFromIdentityStatus::NotStarted + | WithdrawFromIdentityStatus::ErrorMessage(_) => { + amount_input.set_max_amount(Some(max_amount_credits)); + true + } + }; - // Use flooring and format the result with 4 decimal places - let floored_amount = (expected_max_amount * 10_000.0).floor() / 10_000.0; + let response = ui.add_enabled_ui(enabled, |ui| amount_input.show(ui)).inner; - // Set the withdrawal amount to the floored value formatted as a string - self.withdrawal_amount = format!("{:.4}", floored_amount); - } - }); + response.inner.update(&mut self.withdrawal_amount); + // errors are handled inside AmountInput } fn render_address_input(&mut self, ui: &mut Ui) { @@ -138,97 +154,91 @@ impl WithdrawalScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut app_action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Withdrawal") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - let address = if self.withdrawal_address.is_empty() { - None - } else { - match Address::from_str(&self.withdrawal_address) { - Ok(address) => Some(address.assume_checked()), - Err(_) => { - self.withdraw_from_identity_status = - WithdrawFromIdentityStatus::ErrorMessage( - "Invalid withdrawal address".to_string(), - ); - None - } - } - }; - - let message_address = if address.is_some() { - self.withdrawal_address.clone() - } else if let Some(payout_address) = self - .identity - .masternode_payout_address(self.app_context.network) - { - format!("masternode payout address {}", payout_address) - } else if !self.app_context.is_developer_mode() { + let address = if self.withdrawal_address.is_empty() { + None + } else { + match Address::from_str(&self.withdrawal_address) { + Ok(address) => Some(address.assume_checked()), + Err(_) => { self.withdraw_from_identity_status = WithdrawFromIdentityStatus::ErrorMessage( - "No masternode payout address".to_string(), + "Invalid withdrawal address".to_string(), ); - return; - } else { - "to default address".to_string() - }; - - let Some(selected_key) = self.selected_key.as_ref() else { - self.withdraw_from_identity_status = - WithdrawFromIdentityStatus::ErrorMessage("No selected key".to_string()); - return; - }; - - ui.label(format!( - "Are you sure you want to withdraw {} Dash to {}", - self.withdrawal_amount, message_address - )); - let parts: Vec<&str> = self.withdrawal_amount.split('.').collect(); - let mut credits: u128 = 0; - - // Process the whole number part if it exists. - if let Some(whole) = parts.first() { - if let Ok(whole_number) = whole.parse::() { - credits += whole_number * 100_000_000_000; // Whole Dash amount to credits - } + self.confirmation_dialog = None; + return AppAction::None; } + } + }; - // Process the fractional part if it exists. - if let Some(fraction) = parts.get(1) { - let fraction_length = fraction.len(); - let fraction_number = fraction.parse::().unwrap_or(0); - // Calculate the multiplier based on the number of digits in the fraction. - let multiplier = 10u128.pow(11 - fraction_length as u32); - credits += fraction_number * multiplier; // Fractional Dash to credits - } + let message_address = if address.is_some() { + self.withdrawal_address.clone() + } else if let Some(payout_address) = self + .identity + .masternode_payout_address(self.app_context.network) + { + format!("masternode payout address {}", payout_address) + } else if !self.app_context.is_developer_mode() { + self.withdraw_from_identity_status = WithdrawFromIdentityStatus::ErrorMessage( + "No masternode payout address".to_string(), + ); + self.confirmation_dialog = None; + return AppAction::None; + } else { + "to default address".to_string() + }; - if ui.button("Confirm").clicked() { - self.confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.withdraw_from_identity_status = - WithdrawFromIdentityStatus::WaitingForResult(now); - app_action = AppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::WithdrawFromIdentity( - self.identity.clone(), - address, - credits as Credits, - Some(selected_key.id()), - ), - )); - } - if ui.button("Cancel").clicked() { - self.confirmation_popup = false; - } - }); - if !is_open { - self.confirmation_popup = false; + let Some(selected_key) = self.selected_key.as_ref() else { + self.withdraw_from_identity_status = + WithdrawFromIdentityStatus::ErrorMessage("No selected key".to_string()); + self.confirmation_dialog = None; + return AppAction::None; + }; + + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Withdrawal".to_string(), + format!( + "Are you sure you want to withdraw {} to {}", + self.withdrawal_amount + .as_ref() + .expect("Withdrawal amount should be present"), + message_address + ), + ) + .danger_mode(true) // Withdrawal is a destructive operation + }); + + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.withdraw_from_identity_status = + WithdrawFromIdentityStatus::WaitingForResult(now); + + // Use the amount directly from the stored amount + let credits = self + .withdrawal_amount + .as_ref() + .expect("Withdrawal amount should be present") + .value() as u128; + + AppAction::BackendTask(BackendTask::IdentityTask( + IdentityTask::WithdrawFromIdentity( + self.identity.clone(), + address, + credits as Credits, + Some(selected_key.id()), + ), + )) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - app_action } pub fn show_success(&self, ui: &mut Ui) -> AppAction { @@ -449,17 +459,26 @@ impl ScreenLike for WithdrawalScreen { ui.add_space(10.0); // Withdraw button + let button = egui::Button::new(RichText::new("Withdraw").color(Color32::WHITE)) .fill(Color32::from_rgb(0, 128, 255)) .frame(true) .corner_radius(3.0) .min_size(egui::vec2(60.0, 30.0)); - if ui.add(button).clicked() { - self.confirmation_popup = true; + let ready = self.withdrawal_amount.as_ref().is_some(); + + if ui + .add_enabled(ready, button) + .on_disabled_hover_text("Please enter a valid amount to withdraw") + .clicked() + && self.confirmation_dialog.is_none() + { + // Create dialog directly in show_confirmation_popup with correct message + inner_action |= self.show_confirmation_popup(ui); } - if self.confirmation_popup { + if self.confirmation_dialog.is_some() { inner_action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b7fb41094..abc3b0910 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -23,12 +23,15 @@ use crate::ui::tokens::transfer_tokens_screen::TransferTokensScreen; use crate::ui::tokens::view_token_claims_screen::ViewTokenClaimsScreen; use crate::ui::tools::contract_visualizer_screen::ContractVisualizerScreen; use crate::ui::tools::document_visualizer_screen::DocumentVisualizerScreen; +use crate::ui::tools::grovestark_screen::GroveSTARKScreen; +use crate::ui::tools::masternode_list_diff_screen::MasternodeListDiffScreen; use crate::ui::tools::platform_info_screen::PlatformInfoScreen; use crate::ui::tools::proof_log_screen::ProofLogScreen; use crate::ui::tools::proof_visualizer_screen::ProofVisualizerScreen; use crate::ui::wallets::import_wallet_screen::ImportWalletScreen; use crate::ui::wallets::wallets_screen::WalletsBalancesScreen; use contracts_documents::add_contracts_screen::AddContractsScreen; +use contracts_documents::dashpay_coming_soon_screen::DashpayScreen; use contracts_documents::group_actions_screen::GroupActionsScreen; use contracts_documents::register_contract_screen::RegisterDataContractScreen; use contracts_documents::update_contract_screen::UpdateDataContractScreen; @@ -87,8 +90,11 @@ pub enum RootScreenType { RootScreenMyTokenBalances, RootScreenTokenSearch, RootScreenTokenCreator, + RootScreenToolsMasternodeListDiffScreen, RootScreenToolsContractVisualizerScreen, RootScreenToolsPlatformInfoScreen, + RootScreenToolsGroveSTARKScreen, + RootScreenDashpay, } impl RootScreenType { @@ -113,6 +119,9 @@ impl RootScreenType { RootScreenType::RootScreenToolsDocumentVisualizerScreen => 15, RootScreenType::RootScreenToolsContractVisualizerScreen => 16, RootScreenType::RootScreenToolsPlatformInfoScreen => 17, + RootScreenType::RootScreenToolsMasternodeListDiffScreen => 18, + RootScreenType::RootScreenDashpay => 19, + RootScreenType::RootScreenToolsGroveSTARKScreen => 20, } } @@ -137,6 +146,9 @@ impl RootScreenType { 15 => Some(RootScreenType::RootScreenToolsDocumentVisualizerScreen), 16 => Some(RootScreenType::RootScreenToolsContractVisualizerScreen), 17 => Some(RootScreenType::RootScreenToolsPlatformInfoScreen), + 18 => Some(RootScreenType::RootScreenToolsMasternodeListDiffScreen), + 19 => Some(RootScreenType::RootScreenDashpay), + 20 => Some(RootScreenType::RootScreenToolsGroveSTARKScreen), _ => None, } } @@ -161,6 +173,9 @@ impl From for ScreenType { RootScreenType::RootScreenMyTokenBalances => ScreenType::TokenBalances, RootScreenType::RootScreenTokenSearch => ScreenType::TokenSearch, RootScreenType::RootScreenTokenCreator => ScreenType::TokenCreator, + RootScreenType::RootScreenToolsMasternodeListDiffScreen => { + ScreenType::MasternodeListDiff + } RootScreenType::RootScreenToolsDocumentVisualizerScreen => { ScreenType::DocumentsVisualizer } @@ -168,6 +183,8 @@ impl From for ScreenType { ScreenType::ContractsVisualizer } RootScreenType::RootScreenToolsPlatformInfoScreen => ScreenType::PlatformInfo, + RootScreenType::RootScreenToolsGroveSTARKScreen => ScreenType::GroveSTARK, + RootScreenType::RootScreenDashpay => ScreenType::Dashpay, } } } @@ -200,6 +217,7 @@ pub enum ScreenType { RegisterContract, UpdateContract, ProofLog, + MasternodeListDiff, TopUpIdentity(QualifiedIdentity), ScheduledVotes, AddContracts, @@ -207,6 +225,8 @@ pub enum ScreenType { DocumentsVisualizer, ContractsVisualizer, PlatformInfo, + GroveSTARK, + Dashpay, CreateDocument, DeleteDocument, ReplaceDocument, @@ -323,6 +343,8 @@ impl ScreenType { ScreenType::PlatformInfo => { Screen::PlatformInfoScreen(PlatformInfoScreen::new(app_context)) } + ScreenType::GroveSTARK => Screen::GroveSTARKScreen(GroveSTARKScreen::new(app_context)), + ScreenType::Dashpay => Screen::DashpayScreen(DashpayScreen::new(app_context)), ScreenType::CreateDocument => Screen::DocumentActionScreen(DocumentActionScreen::new( app_context.clone(), None, @@ -350,7 +372,6 @@ impl ScreenType { ScreenType::GroupActions => { Screen::GroupActionsScreen(GroupActionsScreen::new(app_context)) } - // Token Screens ScreenType::TokenBalances => Screen::TokensScreen(Box::new(TokensScreen::new( app_context, @@ -406,6 +427,9 @@ impl ScreenType { app_context, ))) } + ScreenType::MasternodeListDiff => { + Screen::MasternodeListDiffScreen(MasternodeListDiffScreen::new(app_context)) + } ScreenType::AddTokenById => Screen::AddTokenById(AddTokenByIdScreen::new(app_context)), ScreenType::PurchaseTokenScreen(identity_token_info) => Screen::PurchaseTokenScreen( PurchaseTokenScreen::new(identity_token_info.clone(), app_context), @@ -422,6 +446,7 @@ pub enum Screen { IdentitiesScreen(IdentitiesScreen), DPNSScreen(DPNSScreen), DocumentQueryScreen(DocumentQueryScreen), + DashpayScreen(DashpayScreen), AddNewWalletScreen(AddNewWalletScreen), ImportWalletScreen(ImportWalletScreen), AddNewIdentityScreen(AddNewIdentityScreen), @@ -445,7 +470,9 @@ pub enum Screen { WalletsBalancesScreen(WalletsBalancesScreen), AddContractsScreen(AddContractsScreen), ProofVisualizerScreen(ProofVisualizerScreen), + MasternodeListDiffScreen(MasternodeListDiffScreen), PlatformInfoScreen(PlatformInfoScreen), + GroveSTARKScreen(GroveSTARKScreen), // Token Screens TokensScreen(Box), @@ -470,6 +497,7 @@ impl Screen { match self { Screen::IdentitiesScreen(screen) => screen.app_context = app_context, Screen::DPNSScreen(screen) => screen.app_context = app_context, + Screen::DashpayScreen(screen) => screen.app_context = app_context, Screen::AddExistingIdentityScreen(screen) => screen.app_context = app_context, Screen::KeyInfoScreen(screen) => screen.app_context = app_context, Screen::KeysScreen(screen) => screen.app_context = app_context, @@ -493,8 +521,19 @@ impl Screen { Screen::ProofLogScreen(screen) => screen.app_context = app_context, Screen::AddContractsScreen(screen) => screen.app_context = app_context, Screen::ProofVisualizerScreen(screen) => screen.app_context = app_context, + Screen::MasternodeListDiffScreen(screen) => { + let old_net = screen.app_context.network; + if old_net != app_context.network { + // Switch context and clear state to avoid cross-network bleed + screen.app_context = app_context.clone(); + screen.clear(); + } else { + screen.app_context = app_context; + } + } Screen::DocumentVisualizerScreen(screen) => screen.app_context = app_context, Screen::PlatformInfoScreen(screen) => screen.app_context = app_context, + Screen::GroveSTARKScreen(screen) => screen.app_context = app_context, // Token Screens Screen::TokensScreen(screen) => screen.app_context = app_context, @@ -578,6 +617,7 @@ impl Screen { dpns_subscreen: DPNSSubscreen::ScheduledVotes, .. }) => ScreenType::ScheduledVotes, + Screen::DashpayScreen(_) => ScreenType::Dashpay, Screen::TransitionVisualizerScreen(_) => ScreenType::TransitionVisualizer, Screen::ContractVisualizerScreen(_) => ScreenType::ContractsVisualizer, Screen::WithdrawalScreen(screen) => { @@ -608,8 +648,10 @@ impl Screen { Screen::ProofLogScreen(_) => ScreenType::ProofLog, Screen::AddContractsScreen(_) => ScreenType::AddContracts, Screen::ProofVisualizerScreen(_) => ScreenType::ProofVisualizer, + Screen::MasternodeListDiffScreen(_) => ScreenType::MasternodeListDiff, Screen::DocumentVisualizerScreen(_) => ScreenType::DocumentsVisualizer, Screen::PlatformInfoScreen(_) => ScreenType::PlatformInfo, + Screen::GroveSTARKScreen(_) => ScreenType::GroveSTARK, // Token Screens Screen::TokensScreen(screen) @@ -682,6 +724,7 @@ impl ScreenLike for Screen { Screen::IdentitiesScreen(screen) => screen.refresh(), Screen::DPNSScreen(screen) => screen.refresh(), Screen::DocumentQueryScreen(screen) => screen.refresh(), + Screen::DashpayScreen(screen) => screen.refresh(), Screen::AddNewWalletScreen(screen) => screen.refresh(), Screen::ImportWalletScreen(screen) => screen.refresh(), Screen::AddNewIdentityScreen(screen) => screen.refresh(), @@ -703,9 +746,11 @@ impl ScreenLike for Screen { Screen::ProofLogScreen(screen) => screen.refresh(), Screen::AddContractsScreen(screen) => screen.refresh(), Screen::ProofVisualizerScreen(screen) => screen.refresh(), + Screen::MasternodeListDiffScreen(screen) => screen.refresh(), Screen::DocumentVisualizerScreen(screen) => screen.refresh(), Screen::ContractVisualizerScreen(screen) => screen.refresh(), Screen::PlatformInfoScreen(screen) => screen.refresh(), + Screen::GroveSTARKScreen(screen) => screen.refresh(), // Token Screens Screen::TokensScreen(screen) => screen.refresh(), @@ -731,6 +776,7 @@ impl ScreenLike for Screen { Screen::IdentitiesScreen(screen) => screen.refresh_on_arrival(), Screen::DPNSScreen(screen) => screen.refresh_on_arrival(), Screen::DocumentQueryScreen(screen) => screen.refresh_on_arrival(), + Screen::DashpayScreen(screen) => screen.refresh_on_arrival(), Screen::AddNewWalletScreen(screen) => screen.refresh_on_arrival(), Screen::ImportWalletScreen(screen) => screen.refresh_on_arrival(), Screen::AddNewIdentityScreen(screen) => screen.refresh_on_arrival(), @@ -752,9 +798,11 @@ impl ScreenLike for Screen { Screen::ProofLogScreen(screen) => screen.refresh_on_arrival(), Screen::AddContractsScreen(screen) => screen.refresh_on_arrival(), Screen::ProofVisualizerScreen(screen) => screen.refresh_on_arrival(), + Screen::MasternodeListDiffScreen(screen) => screen.refresh_on_arrival(), Screen::DocumentVisualizerScreen(screen) => screen.refresh_on_arrival(), Screen::ContractVisualizerScreen(screen) => screen.refresh_on_arrival(), Screen::PlatformInfoScreen(screen) => screen.refresh_on_arrival(), + Screen::GroveSTARKScreen(screen) => screen.refresh_on_arrival(), // Token Screens Screen::TokensScreen(screen) => screen.refresh_on_arrival(), @@ -780,6 +828,7 @@ impl ScreenLike for Screen { Screen::IdentitiesScreen(screen) => screen.ui(ctx), Screen::DPNSScreen(screen) => screen.ui(ctx), Screen::DocumentQueryScreen(screen) => screen.ui(ctx), + Screen::DashpayScreen(screen) => screen.ui(ctx), Screen::AddNewWalletScreen(screen) => screen.ui(ctx), Screen::ImportWalletScreen(screen) => screen.ui(ctx), Screen::AddNewIdentityScreen(screen) => screen.ui(ctx), @@ -801,9 +850,11 @@ impl ScreenLike for Screen { Screen::ProofLogScreen(screen) => screen.ui(ctx), Screen::AddContractsScreen(screen) => screen.ui(ctx), Screen::ProofVisualizerScreen(screen) => screen.ui(ctx), + Screen::MasternodeListDiffScreen(screen) => screen.ui(ctx), Screen::DocumentVisualizerScreen(screen) => screen.ui(ctx), Screen::ContractVisualizerScreen(screen) => screen.ui(ctx), Screen::PlatformInfoScreen(screen) => screen.ui(ctx), + Screen::GroveSTARKScreen(screen) => screen.ui(ctx), // Token Screens Screen::TokensScreen(screen) => screen.ui(ctx), @@ -829,6 +880,7 @@ impl ScreenLike for Screen { Screen::IdentitiesScreen(screen) => screen.display_message(message, message_type), Screen::DPNSScreen(screen) => screen.display_message(message, message_type), Screen::DocumentQueryScreen(screen) => screen.display_message(message, message_type), + Screen::DashpayScreen(screen) => screen.display_message(message, message_type), Screen::AddNewWalletScreen(screen) => screen.display_message(message, message_type), Screen::ImportWalletScreen(screen) => screen.display_message(message, message_type), Screen::AddNewIdentityScreen(screen) => screen.display_message(message, message_type), @@ -858,6 +910,9 @@ impl ScreenLike for Screen { Screen::ProofLogScreen(screen) => screen.display_message(message, message_type), Screen::AddContractsScreen(screen) => screen.display_message(message, message_type), Screen::ProofVisualizerScreen(screen) => screen.display_message(message, message_type), + Screen::MasternodeListDiffScreen(screen) => { + screen.display_message(message, message_type) + } Screen::DocumentVisualizerScreen(screen) => { screen.display_message(message, message_type) } @@ -865,6 +920,7 @@ impl ScreenLike for Screen { screen.display_message(message, message_type) } Screen::PlatformInfoScreen(screen) => screen.display_message(message, message_type), + Screen::GroveSTARKScreen(screen) => screen.display_message(message, message_type), // Token Screens Screen::TokensScreen(screen) => screen.display_message(message, message_type), @@ -898,6 +954,9 @@ impl ScreenLike for Screen { Screen::DocumentQueryScreen(screen) => { screen.display_task_result(backend_task_success_result) } + Screen::DashpayScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } Screen::AddNewWalletScreen(screen) => { screen.display_task_result(backend_task_success_result) } @@ -960,12 +1019,18 @@ impl ScreenLike for Screen { Screen::ProofVisualizerScreen(screen) => { screen.display_task_result(backend_task_success_result) } + Screen::MasternodeListDiffScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } Screen::ContractVisualizerScreen(screen) => { screen.display_task_result(backend_task_success_result) } Screen::PlatformInfoScreen(screen) => { screen.display_task_result(backend_task_success_result) } + Screen::GroveSTARKScreen(screen) => { + screen.display_task_result(backend_task_success_result) + } // Token Screens Screen::TokensScreen(screen) => screen.display_task_result(backend_task_success_result), @@ -1017,6 +1082,7 @@ impl ScreenLike for Screen { Screen::IdentitiesScreen(screen) => screen.pop_on_success(), Screen::DPNSScreen(screen) => screen.pop_on_success(), Screen::DocumentQueryScreen(screen) => screen.pop_on_success(), + Screen::DashpayScreen(screen) => screen.pop_on_success(), Screen::AddNewWalletScreen(screen) => screen.pop_on_success(), Screen::ImportWalletScreen(screen) => screen.pop_on_success(), Screen::AddNewIdentityScreen(screen) => screen.pop_on_success(), @@ -1038,9 +1104,11 @@ impl ScreenLike for Screen { Screen::ProofLogScreen(screen) => screen.pop_on_success(), Screen::AddContractsScreen(screen) => screen.pop_on_success(), Screen::ProofVisualizerScreen(screen) => screen.pop_on_success(), + Screen::MasternodeListDiffScreen(screen) => screen.pop_on_success(), Screen::DocumentVisualizerScreen(screen) => screen.pop_on_success(), Screen::ContractVisualizerScreen(screen) => screen.pop_on_success(), Screen::PlatformInfoScreen(screen) => screen.pop_on_success(), + Screen::GroveSTARKScreen(screen) => screen.pop_on_success(), // Token Screens Screen::TokensScreen(screen) => screen.pop_on_success(), diff --git a/src/ui/network_chooser_screen.rs b/src/ui/network_chooser_screen.rs index dc76f4f29..d9a4f1e26 100644 --- a/src/ui/network_chooser_screen.rs +++ b/src/ui/network_chooser_screen.rs @@ -46,7 +46,6 @@ impl NetworkChooserScreen { devnet_app_context: Option<&Arc>, local_app_context: Option<&Arc>, current_network: Network, - custom_dash_qt_path: Option, overwrite_dash_conf: bool, ) -> Self { let local_network_dashmate_password = if let Ok(config) = Config::load() { @@ -68,13 +67,14 @@ impl NetworkChooserScreen { }; let developer_mode = current_context.is_developer_mode(); - // Load theme preference from settings - let theme_preference = current_context + // Load settings including theme preference and dash_qt_path + let settings = current_context .get_settings() .ok() .flatten() - .map(|(_, _, _, _, _, theme)| theme) - .unwrap_or(ThemeMode::System); + .unwrap_or_default(); + let theme_preference = settings.theme_mode; + let custom_dash_qt_path = settings.dash_qt_path; Self { mainnet_app_context: mainnet_app_context.clone(), @@ -122,7 +122,6 @@ impl NetworkChooserScreen { /// TODO: doesn't save local network settings like password yet. fn save(&self) -> Result<(), String> { self.current_app_context() - .db .update_dash_core_execution_settings( self.custom_dash_qt_path.clone(), self.overwrite_dash_conf, @@ -227,8 +226,7 @@ impl NetworkChooserScreen { .min_size(egui::vec2(120.0, 32.0)), ) .clicked() - { - if let Some(path) = rfd::FileDialog::new().pick_file() { + && let Some(path) = rfd::FileDialog::new().pick_file() { let file_name = path.file_name().and_then(|f| f.to_str()); if let Some(file_name) = file_name { @@ -275,7 +273,6 @@ impl NetworkChooserScreen { } } } - } if (self.custom_dash_qt_path.is_some() || self.custom_dash_qt_error_message.is_some()) @@ -289,7 +286,7 @@ impl NetworkChooserScreen { ) .clicked() { - self.custom_dash_qt_path = None; + self.custom_dash_qt_path = Some(PathBuf::new()); // Reset to empty to avoid auto-detection self.custom_dash_qt_error_message = None; self.save().expect("Expected to save db settings"); } @@ -564,12 +561,18 @@ impl NetworkChooserScreen { } // Add a button to start the network + let start_enabled = if let Some(path) = self.custom_dash_qt_path.as_ref() { + !path.as_os_str().is_empty() && path.is_file() + } else { + false + }; + if network != Network::Regtest { - ui.add_enabled_ui(self.custom_dash_qt_path.is_some(), |ui| { + ui.add_enabled_ui(start_enabled, |ui| { if ui .button("Start") .on_disabled_hover_text( - "Configure dash-qt binary using Advanced Settings below", + "Please select path to dash-qt binary in Advanced Settings", ) .clicked() { @@ -597,35 +600,31 @@ impl NetworkChooserScreen { ); if ui.button("Save Password").clicked() { // 1) Reload the config - if let Ok(mut config) = Config::load() { - if let Some(local_cfg) = config.config_for_network(Network::Regtest).clone() { - let updated_local_config = local_cfg - .update_core_rpc_password(self.local_network_dashmate_password.clone()); - config.update_config_for_network( - Network::Regtest, - updated_local_config.clone(), - ); - if let Err(e) = config.save() { - eprintln!("Failed to save config to .env: {e}"); + if let Ok(mut config) = Config::load() + && let Some(local_cfg) = config.config_for_network(Network::Regtest).clone() + { + let updated_local_config = local_cfg + .update_core_rpc_password(self.local_network_dashmate_password.clone()); + config + .update_config_for_network(Network::Regtest, updated_local_config.clone()); + if let Err(e) = config.save() { + eprintln!("Failed to save config to .env: {e}"); + } + + // 5) Update our local AppContext in memory + if let Some(local_app_context) = &self.local_app_context { + { + // Overwrite the config field with the new password + let mut cfg_lock = local_app_context.config.write().unwrap(); + *cfg_lock = updated_local_config; } - // 5) Update our local AppContext in memory - if let Some(local_app_context) = &self.local_app_context { - { - // Overwrite the config field with the new password - let mut cfg_lock = local_app_context.config.write().unwrap(); - *cfg_lock = updated_local_config; - } - - // 6) Re-init the client & sdk from the updated config - if let Err(e) = - Arc::clone(local_app_context).reinit_core_client_and_sdk() - { - eprintln!("Failed to re-init local RPC client and sdk: {}", e); - } else { - // Trigger SwitchNetworks - app_action = AppAction::SwitchNetwork(Network::Regtest); - } + // 6) Re-init the client & sdk from the updated config + if let Err(e) = Arc::clone(local_app_context).reinit_core_client_and_sdk() { + eprintln!("Failed to re-init local RPC client and sdk: {}", e); + } else { + // Trigger SwitchNetworks + app_action = AppAction::SwitchNetwork(Network::Regtest); } } } @@ -666,12 +665,10 @@ impl ScreenLike for NetworkChooserScreen { self.should_reset_collapsing_states = true; // Reload settings from database to ensure we have the latest values - if let Ok(Some((_, _, _, custom_dash_qt_path, overwrite_dash_conf, theme_preference))) = - self.current_app_context().get_settings() - { - self.custom_dash_qt_path = custom_dash_qt_path; - self.overwrite_dash_conf = overwrite_dash_conf; - self.theme_preference = theme_preference; + if let Ok(Some(settings)) = self.current_app_context().get_settings() { + self.custom_dash_qt_path = settings.dash_qt_path; + self.overwrite_dash_conf = settings.overwrite_dash_conf; + self.theme_preference = settings.theme_mode; } } @@ -728,7 +725,7 @@ impl ScreenLike for NetworkChooserScreen { action |= island_central_panel(ctx, |ui| { egui::ScrollArea::vertical() - .auto_shrink([false; 2]) + .auto_shrink([true; 2]) .show(ui, |ui| self.render_network_table(ui)) .inner }); diff --git a/src/ui/theme.rs b/src/ui/theme.rs index e83307f1d..4b2be45d8 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -10,18 +10,20 @@ pub enum ThemeMode { } /// Detect system theme preference -pub fn detect_system_theme() -> ThemeMode { - match dark_light::detect() { - dark_light::Mode::Dark => ThemeMode::Dark, - dark_light::Mode::Light => ThemeMode::Light, - dark_light::Mode::Default => ThemeMode::Light, // Default to light if unknown +pub fn detect_system_theme() -> Result { + match dark_light::detect().map_err(|e| e.to_string())? { + dark_light::Mode::Dark => Ok(ThemeMode::Dark), + dark_light::Mode::Light => Ok(ThemeMode::Light), + dark_light::Mode::Unspecified => Ok(ThemeMode::Light), // Default to light if unknown } } /// Resolve the actual theme to use based on preference pub fn resolve_theme_mode(preference: ThemeMode) -> ThemeMode { match preference { - ThemeMode::System => detect_system_theme(), + ThemeMode::System => detect_system_theme() + .inspect_err(|e| tracing::warn!("Failed to detect system theme: {}", e)) + .unwrap_or(ThemeMode::Light), other => other, } } @@ -451,6 +453,10 @@ impl ComponentStyles { DashColors::WHITE } + pub fn primary_button_stroke() -> Stroke { + Stroke::new(1.0, DashColors::DASH_BLUE) + } + pub fn secondary_button_fill() -> Color32 { DashColors::WHITE } diff --git a/src/ui/tokens/add_token_by_id_screen.rs b/src/ui/tokens/add_token_by_id_screen.rs index 7c35155e9..4e4c5e183 100644 --- a/src/ui/tokens/add_token_by_id_screen.rs +++ b/src/ui/tokens/add_token_by_id_screen.rs @@ -128,33 +128,31 @@ impl AddTokenByIdScreen { } fn render_add_button(&mut self, ui: &mut Ui) -> AppAction { - if let (Some(contract), Some(tok)) = (&self.fetched_contract, &self.selected_token) { - if ui + if let (Some(contract), Some(tok)) = (&self.fetched_contract, &self.selected_token) + && ui .add( egui::Button::new(RichText::new("Add Token").color(Color32::WHITE)) .fill(Color32::from_rgb(0, 120, 0)), ) .clicked() - { - let insert_mode = - InsertTokensToo::SomeTokensShouldBeAdded(vec![tok.token_position]); - - // Set status to show we're processing - self.status = AddTokenStatus::Searching(chrono::Utc::now().timestamp() as u32); - - // None for alias; change if you allow user alias input - return AppAction::BackendTasks( - vec![ - BackendTask::ContractTask(Box::new(ContractTask::SaveDataContract( - contract.clone(), - None, - insert_mode, - ))), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - crate::app::BackendTasksExecutionMode::Sequential, - ); - } + { + let insert_mode = InsertTokensToo::SomeTokensShouldBeAdded(vec![tok.token_position]); + + // Set status to show we're processing + self.status = AddTokenStatus::Searching(chrono::Utc::now().timestamp() as u32); + + // None for alias; change if you allow user alias input + return AppAction::BackendTasks( + vec![ + BackendTask::ContractTask(Box::new(ContractTask::SaveDataContract( + contract.clone(), + None, + insert_mode, + ))), + BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), + ], + crate::app::BackendTasksExecutionMode::Sequential, + ); } AppAction::None } diff --git a/src/ui/tokens/burn_tokens_screen.rs b/src/ui/tokens/burn_tokens_screen.rs index 96a3ac708..39fe3ec8d 100644 --- a/src/ui/tokens/burn_tokens_screen.rs +++ b/src/ui/tokens/burn_tokens_screen.rs @@ -1,9 +1,13 @@ +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; +use crate::ui::components::{Component, ComponentResponse}; use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; use crate::ui::helpers::{TransactionType, add_identity_key_chooser, render_group_action_text}; use crate::ui::theme::DashColors; +use crate::ui::tokens::tokens_screen::IdentityTokenIdentifier; use dash_sdk::dpp::data_contract::GroupContractPosition; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; @@ -25,6 +29,7 @@ use crate::app::{AppAction, BackendTasksExecutionMode}; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; +use crate::model::amount::Amount; use crate::model::wallet::Wallet; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; @@ -52,7 +57,9 @@ pub struct BurnTokensScreen { pub group_action_id: Option, // The user chooses how many tokens to burn - pub amount_to_burn: String, + pub amount: Option, + pub amount_input: Option, + pub max_amount: Option, // Maximum amount the user can burn based on their balance pub public_note: Option, status: BurnTokensStatus, @@ -62,7 +69,7 @@ pub struct BurnTokensScreen { pub app_context: Arc, // Confirmation popup - show_confirmation_popup: bool, + confirmation_dialog: Option, // For password-based wallet unlocking, if needed selected_wallet: Option>>, @@ -72,6 +79,18 @@ pub struct BurnTokensScreen { impl BurnTokensScreen { pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let token_balance = match app_context.identity_token_balances() { + Ok(identity_token_balances) => { + let itb = identity_token_balances; + let key = IdentityTokenIdentifier { + identity_id: identity_token_info.identity.identity.id(), + token_id: identity_token_info.token_id, + }; + itb.get(&key).map(|itb| itb.balance) + } + Err(_) => None, + }; + let possible_key = identity_token_info .identity .identity @@ -151,17 +170,17 @@ impl BurnTokensScreen { }; let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } + if group.is_some() + && let Some((_, group)) = group.clone() + { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power + && your_power >= &group.required_power() + { + is_unilateral_group_member = true; } }; @@ -179,12 +198,14 @@ impl BurnTokensScreen { group, is_unilateral_group_member, group_action_id: None, - amount_to_burn: String::new(), + amount: None, + amount_input: None, + max_amount: token_balance, public_note: None, status: BurnTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -192,98 +213,101 @@ impl BurnTokensScreen { } /// Renders a text input for the user to specify an amount to burn - fn render_amount_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Amount to Burn:"); - ui.text_edit_singleline(&mut self.amount_to_burn); + fn render_amount_input(&mut self, ui: &mut egui::Ui) { + let amount_input = self.amount_input.get_or_insert_with(|| { + let token_amount = Amount::from_token(&self.identity_token_info, 0); + let mut input = AmountInput::new(token_amount).with_label("Amount:"); + + if self.max_amount.is_some() { + input.set_show_max_button(self.max_amount.is_some()); + input.set_max_amount(self.max_amount); + } + + input }); + + let amount_response = amount_input.show(ui).inner; + // Update the amount based on user input + amount_response.update(&mut self.amount); + // errors are handled inside AmountInput } /// Renders a confirm popup with the final "Are you sure?" step fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Burn") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Validate user input - let amount_ok = self.amount_to_burn.parse::().ok(); - if amount_ok.is_none() { - self.error_message = Some("Please enter a valid integer amount.".into()); - self.status = BurnTokensStatus::ErrorMessage("Invalid amount".into()); - self.show_confirmation_popup = false; - return; - } - - ui.label(format!( - "Are you sure you want to burn {} tokens?", - self.amount_to_burn - )); - - ui.add_space(10.0); - - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = BurnTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch the actual backend burn action - action = AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::BurnTokens { - owner_identity: self.identity_token_info.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - amount: amount_ok.unwrap(), - group_info, - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } + let amount = match self.amount.as_ref() { + Some(amount) if amount.value() > 0 => amount, + _ => { + self.error_message = Some("Please enter a valid amount greater than 0.".into()); + self.status = BurnTokensStatus::ErrorMessage("Invalid amount".into()); + self.confirmation_dialog = None; + return AppAction::None; + } + }; - // Cancel button - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Burn".to_string(), + format!("Are you sure you want to burn {}?", amount), + ) + .danger_mode(true) // Burning tokens is destructive + }); - if !is_open { - self.show_confirmation_popup = false; + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = BurnTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = + Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Dispatch the actual backend burn action + AppAction::BackendTasks( + vec![ + BackendTask::TokenTask(Box::new(TokenTask::BurnTokens { + owner_identity: self.identity_token_info.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("Expected a key"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + amount: amount.value(), + group_info, + })), + BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), + ], + BackendTasksExecutionMode::Sequential, + ) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } /// Renders a simple "Success!" screen after completion @@ -352,13 +376,12 @@ impl ScreenLike for BurnTokensScreen { fn refresh(&mut self) { // If you need to reload local identity data or re-check keys - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities + if let Ok(all_identities) = self.app_context.load_local_user_identities() + && let Some(updated_identity) = all_identities .into_iter() .find(|id| id.identity.id() == self.identity_token_info.identity.identity.id()) - { - self.identity_token_info.identity = updated_identity; - } + { + self.identity_token_info.identity = updated_identity; } } @@ -504,7 +527,13 @@ impl ScreenLike for BurnTokensScreen { "You are signing an existing group Burn so you are not allowed to choose the amount.", ); ui.add_space(5.0); - ui.label(format!("Amount: {}", self.amount_to_burn)); + ui.label(format!( + "Amount: {}", + self.amount + .as_ref() + .map(|a| a.to_string()) + .unwrap_or_default() + )); } else { self.render_amount_input(ui); } @@ -559,12 +588,26 @@ impl ScreenLike for BurnTokensScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + // Create confirmation dialog on button click + if self.confirmation_dialog.is_none() { + let amount = match self.amount.as_ref() { + Some(amount) if amount.value() > 0 => amount, + _ => return AppAction::None, + }; + + self.confirmation_dialog = Some( + ConfirmationDialog::new( + "Confirm Burn".to_string(), + format!("Are you sure you want to burn {}?", amount), + ) + .danger_mode(true), + ); + } } } - // If user pressed "Burn," show a popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index 918ef055f..6c6083666 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -1,3 +1,5 @@ +use crate::ui::components::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -54,7 +56,7 @@ pub struct ClaimTokensScreen { status: ClaimTokensStatus, error_message: Option, pub app_context: Arc, - show_confirmation_popup: bool, + confirmation_dialog: Option, selected_wallet: Option>>, wallet_password: String, show_password: bool, @@ -122,7 +124,7 @@ impl ClaimTokensScreen { status: ClaimTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -181,52 +183,47 @@ impl ClaimTokensScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; let distribution_type = self .distribution_type .unwrap_or(TokenDistributionType::Perpetual); - egui::Window::new("Confirm Claim") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label("Are you sure you want to claim tokens for this contract?"); - ui.add_space(10.0); - - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = ClaimTokensStatus::WaitingForResult(now); - - action = AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::ClaimTokens { - data_contract: Arc::new(self.token_contract.contract.clone()), - token_position: self.identity_token_basic_info.token_position, - actor_identity: self.identity.clone(), - distribution_type, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: self.public_note.clone(), - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Claim".to_string(), + "Are you sure you want to claim tokens for this contract?".to_string(), + ) + }); - if !is_open { - self.show_confirmation_popup = false; + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = ClaimTokensStatus::WaitingForResult(now); + + AppAction::BackendTasks( + vec![ + BackendTask::TokenTask(Box::new(TokenTask::ClaimTokens { + data_contract: Arc::new(self.token_contract.contract.clone()), + token_position: self.identity_token_basic_info.token_position, + actor_identity: self.identity.clone(), + distribution_type, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: self.public_note.clone(), + })), + BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), + ], + BackendTasksExecutionMode::Sequential, + ) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } fn show_success_screen(&self, ui: &mut Ui) -> AppAction { @@ -266,13 +263,12 @@ impl ScreenLike for ClaimTokensScreen { } fn refresh(&mut self) { - if let Ok(all) = self.app_context.load_local_qualified_identities() { - if let Some(updated) = all + if let Ok(all) = self.app_context.load_local_qualified_identities() + && let Some(updated) = all .into_iter() .find(|id| id.identity.id() == self.identity.identity.id()) - { - self.identity = updated; - } + { + self.identity = updated; } } @@ -511,13 +507,16 @@ impl ScreenLike for ClaimTokensScreen { "Please select a distribution type.".to_string(), ); return; - } else { - self.show_confirmation_popup = true; + } else if self.confirmation_dialog.is_none() { + self.confirmation_dialog = Some(ConfirmationDialog::new( + "Confirm Claim".to_string(), + "Are you sure you want to claim tokens for this contract?".to_string(), + )); } } - // If user pressed "Claim," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/destroy_frozen_funds_screen.rs b/src/ui/tokens/destroy_frozen_funds_screen.rs index cb9249bba..4961bc83e 100644 --- a/src/ui/tokens/destroy_frozen_funds_screen.rs +++ b/src/ui/tokens/destroy_frozen_funds_screen.rs @@ -1,10 +1,13 @@ use super::tokens_screen::IdentityTokenInfo; -use crate::app::{AppAction, BackendTasksExecutionMode}; +use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -65,14 +68,18 @@ pub struct DestroyFrozenFundsScreen { /// Typically some Identity that has been frozen by the system or a group pub frozen_identity_id: String, + /// All frozen identities that can be selected + /// TODO: We should filter them by frozen status, right now we just show all known identities + pub frozen_identities: Vec, + status: DestroyFrozenFundsStatus, error_message: Option, /// Basic references pub app_context: Arc, - /// Confirmation popup - show_confirmation_popup: bool, + /// Confirmation dialog + confirmation_dialog: Option, /// If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -161,17 +168,17 @@ impl DestroyFrozenFundsScreen { }; let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } + if group.is_some() + && let Some((_, group)) = group.clone() + { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power + && your_power >= &group.required_power() + { + is_unilateral_group_member = true; } }; @@ -183,9 +190,14 @@ impl DestroyFrozenFundsScreen { &mut error_message, ); + let all_identities = app_context + .load_local_qualified_identities() + .expect("Identities not loaded"); + Self { identity: identity_token_info.identity.clone(), frozen_identity_id: String::new(), + frozen_identities: all_identities, identity_token_info, selected_key: possible_key, group, @@ -195,7 +207,7 @@ impl DestroyFrozenFundsScreen { status: DestroyFrozenFundsStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -204,108 +216,99 @@ impl DestroyFrozenFundsScreen { /// Renders the text input for specifying the “frozen identity” fn render_frozen_identity_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Frozen Identity ID:"); - ui.text_edit_singleline(&mut self.frozen_identity_id); - }); + ui.add( + IdentitySelector::new( + "frozen_identity_selector", + &mut self.frozen_identity_id, + &self.frozen_identities, + ) + .label("Frozen Identity ID:"), + ); } /// Confirmation popup fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Destroy Frozen Funds") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Parse the user input into an Identifier - let maybe_frozen_id = Identifier::from_string_try_encodings( - &self.frozen_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - - if maybe_frozen_id.is_err() { - self.error_message = Some("Invalid frozen identity format".into()); - self.status = DestroyFrozenFundsStatus::ErrorMessage("Invalid identity".into()); - self.show_confirmation_popup = false; - return; - } - - let frozen_id = maybe_frozen_id.unwrap(); - - ui.label(format!( - "Are you sure you want to destroy the frozen funds of identity {}?", - self.frozen_identity_id - )); - - ui.add_space(10.0); - - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = DestroyFrozenFundsStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch the actual backend destroy action - action = AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::DestroyFrozenFunds { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - frozen_identity: frozen_id, - group_info, - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } + let msg = format!( + "Are you sure you want to destroy frozen funds for identity {}? This action cannot be undone.", + self.frozen_identity_id + ); - // Cancel - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Destroy Frozen Funds", msg) + .confirm_text(Some("Destroy")) + .cancel_text(Some("Cancel")) + .danger_mode(true) + }); - if !is_open { - self.show_confirmation_popup = false; + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } + fn confirmation_ok(&mut self) -> AppAction { + let maybe_frozen_id = Identifier::from_string_try_encodings( + &self.frozen_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + if maybe_frozen_id.is_err() { + self.error_message = Some("Invalid frozen identity format".into()); + self.status = DestroyFrozenFundsStatus::ErrorMessage("Invalid identity".into()); + return AppAction::None; + } + let frozen_id = maybe_frozen_id.unwrap(); + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = DestroyFrozenFundsStatus::WaitingForResult(now); + + let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::DestroyFrozenFunds { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + frozen_identity: frozen_id, + group_info, + }, + ))) + } /// Simple “Success” screen fn show_success_screen(&self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; @@ -376,13 +379,12 @@ impl ScreenLike for DestroyFrozenFundsScreen { fn refresh(&mut self) { // Reload the identity data if needed - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities + if let Ok(all_identities) = self.app_context.load_local_user_identities() + && let Some(updated_identity) = all_identities .into_iter() .find(|id| id.identity.id() == self.identity.identity.id()) - { - self.identity = updated_identity; - } + { + self.identity = updated_identity; } } @@ -571,12 +573,22 @@ impl ScreenLike for DestroyFrozenFundsScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + // Initialize confirmation dialog when button is clicked + let msg = format!( + "Are you sure you want to destroy frozen funds for identity {}? This action cannot be undone.", + self.frozen_identity_id + ); + self.confirmation_dialog = Some( + ConfirmationDialog::new("Confirm Destroy Frozen Funds", msg) + .confirm_text(Some("Destroy")) + .cancel_text(Some("Cancel")) + .danger_mode(true), + ); } } - // If user pressed "Destroy," show a popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/direct_token_purchase_screen.rs b/src/ui/tokens/direct_token_purchase_screen.rs index 8afabb88a..4e2de7d98 100644 --- a/src/ui/tokens/direct_token_purchase_screen.rs +++ b/src/ui/tokens/direct_token_purchase_screen.rs @@ -3,6 +3,8 @@ use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; use dash_sdk::dpp::fee::Credits; use dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use eframe::egui::{self, Color32, Context, Ui}; @@ -13,12 +15,16 @@ use crate::app::{AppAction, BackendTasksExecutionMode}; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; +use crate::model::amount::{Amount, DASH_DECIMAL_PLACES}; use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::components::{Component, ComponentResponse}; use crate::ui::helpers::{TransactionType, add_identity_key_chooser}; use crate::ui::identities::get_selected_wallet; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; @@ -45,15 +51,15 @@ pub struct PurchaseTokenScreen { pub identity_token_info: IdentityTokenInfo, selected_key: Option, - // Specific to this transition - amount_to_purchase: String, - total_agreed_price: String, + // Specific to this transition - using AmountInput components following design pattern + amount_to_purchase_input: Option, + amount_to_purchase_value: Option, fetched_pricing_schedule: Option, - calculated_price: Option, + calculated_price_credits: Option, pricing_fetch_attempted: bool, /// Screen stuff - show_confirmation_popup: bool, + confirmation_dialog: Option, status: PurchaseTokensStatus, error_message: Option, @@ -89,32 +95,50 @@ impl PurchaseTokenScreen { Self { identity_token_info, selected_key: possible_key, - amount_to_purchase: "".to_string(), - total_agreed_price: "".to_string(), + amount_to_purchase_input: None, + amount_to_purchase_value: None, fetched_pricing_schedule: None, - calculated_price: None, + calculated_price_credits: None, pricing_fetch_attempted: false, status: PurchaseTokensStatus::NotStarted, error_message: None, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, } } - /// Renders a text input for the user to specify an amount to purchase + /// Renders AmountInput components for the user to specify an amount to purchase fn render_amount_input(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; ui.horizontal(|ui| { - ui.label("Amount to Purchase:"); - let response = ui.text_edit_singleline(&mut self.amount_to_purchase); + // Use AmountInput for token amount with lazy initialization + let amount_input = self.amount_to_purchase_input.get_or_insert_with(|| { + AmountInput::new( + Amount::new( + 0, + self.identity_token_info + .token_config + .conventions() + .decimals(), + ) + .with_unit_name(&self.identity_token_info.token_alias), + ) + .with_label("Amount to Purchase:") + .with_hint_text("Enter token amount to purchase") + .with_min_amount(Some(1)) + }); + + let response = amount_input.show(ui); + response.inner.update(&mut self.amount_to_purchase_value); - // When amount changes, recalculate the price if we have pricing schedule - if response.changed() { + // When amount changes, update domain data and recalculate the price + if response.inner.has_changed() { self.recalculate_price(); + self.confirmation_dialog = None; } // Fetch pricing button @@ -142,14 +166,46 @@ impl PurchaseTokenScreen { if let Some(pricing_schedule) = &self.fetched_pricing_schedule { ui.add_space(5.0); ui.label("Current pricing:"); + let dark_mode = ui.ctx().style().visuals.dark_mode; + match pricing_schedule { - TokenPricingSchedule::SinglePrice(price) => { - ui.label(format!(" Fixed price: {} credits per token", price)); + TokenPricingSchedule::SinglePrice(price_per_unit) => { + // Convert price per smallest unit to price per whole token for display, guarding for the minimal + // representable value (using Amount ref display which pads decimals properly) + if *price_per_unit == 0 { + ui.colored_label( + DashColors::error_color(dark_mode), + " Fixed price: FREE (pricing schedule stores 0 credits per unit)", + ); + } else { + let price_per_token = (*price_per_unit as u128) + .saturating_mul(self.token_decimal_multiplier() as u128) + .min(u64::MAX as u128) + as u64; + let price = Amount::new(price_per_token, DASH_DECIMAL_PLACES) + .with_unit_name("DASH"); + ui.label(format!(" Fixed price: {} per token", price)); + } } TokenPricingSchedule::SetPrices(tiers) => { ui.label(" Tiered pricing:"); - for (amount, price) in tiers { - ui.label(format!(" {} tokens: {} credits each", amount, price)); + for (amount_value, price_per_unit) in tiers { + let amount = Amount::from_token(&self.identity_token_info, *amount_value); + // Convert price per smallest unit to price per token for display + if *price_per_unit == 0 { + ui.colored_label( + DashColors::error_color(dark_mode), + format!(" {} tokens: FREE (tier stores 0 credits)", amount), + ); + } else { + let price_per_token = (*price_per_unit as u128) + .saturating_mul(self.token_decimal_multiplier() as u128) + .min(u64::MAX as u128) + as u64; + let price = Amount::new(price_per_token, DASH_DECIMAL_PLACES) + .with_unit_name("DASH"); + ui.label(format!(" {} tokens: {} each", amount, price)); + } } } } @@ -160,11 +216,12 @@ impl PurchaseTokenScreen { /// Recalculates the total price based on amount and pricing schedule fn recalculate_price(&mut self) { - if let (Some(pricing_schedule), Ok(amount)) = ( + if let (Some(pricing_schedule), Some(amount_value)) = ( &self.fetched_pricing_schedule, - self.amount_to_purchase.parse::(), + &self.amount_to_purchase_value, ) { - let price_per_token = match pricing_schedule { + let amount = amount_value.value(); + let price_per_unit = match pricing_schedule { TokenPricingSchedule::SinglePrice(price) => *price, TokenPricingSchedule::SetPrices(tiers) => { // Find the appropriate tier for this amount @@ -178,98 +235,79 @@ impl PurchaseTokenScreen { } }; - let total_price = amount.saturating_mul(price_per_token); - self.calculated_price = Some(total_price); - self.total_agreed_price = total_price.to_string(); + // The price from Platform is per smallest unit, and amount is in smallest units + // So we multiply them directly using wider arithmetic to avoid overflow + let total_price = (amount as u128) + .saturating_mul(price_per_unit as u128) + .min(u64::MAX as u128) as u64; + self.calculated_price_credits = Some(total_price); } else { - self.calculated_price = None; + self.calculated_price_credits = None; } } + fn token_decimal_multiplier(&self) -> u64 { + 10u64.pow( + self.identity_token_info + .token_config + .conventions() + .decimals() as u32, + ) + } + /// Renders a confirm popup with the final "Are you sure?" step fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Purchase") - .collapsible(false) - .open(&mut is_open) - .frame( - egui::Frame::default() - .fill(egui::Color32::from_rgb(245, 245, 245)) - .stroke(egui::Stroke::new( - 1.0, - egui::Color32::from_rgb(200, 200, 200), - )) - .shadow(egui::epaint::Shadow::default()) - .inner_margin(egui::Margin::same(20)) - .corner_radius(egui::CornerRadius::same(8)), - ) - .show(ui.ctx(), |ui| { - // Validate user input - let amount_ok = self.amount_to_purchase.parse::().ok(); - if amount_ok.is_none() { - self.error_message = Some("Please enter a valid amount.".into()); - self.status = PurchaseTokensStatus::ErrorMessage("Invalid amount".into()); - self.show_confirmation_popup = false; - return; - } - - let total_agreed_price_ok: Option = - self.total_agreed_price.parse::().ok(); - if total_agreed_price_ok.is_none() { - self.error_message = Some("Please enter a valid total agreed price.".into()); - self.status = - PurchaseTokensStatus::ErrorMessage("Invalid total agreed price".into()); - self.show_confirmation_popup = false; - return; - } - - ui.label(format!( - "Are you sure you want to purchase {} token(s) for {} Credits?", - self.amount_to_purchase, self.total_agreed_price - )); - - ui.add_space(10.0); - - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = PurchaseTokensStatus::WaitingForResult(now); - - // Dispatch the actual backend purchase action - action = AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::PurchaseTokens { - identity: self.identity_token_info.identity.clone(), - data_contract: Arc::new( - self.identity_token_info.data_contract.contract.clone(), - ), - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - amount: amount_ok.expect("Expected a valid amount"), - total_agreed_price: total_agreed_price_ok - .expect("Expected a valid total agreed price"), - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } - - // Cancel button - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); - - if !is_open { - self.show_confirmation_popup = false; + let Some(amount) = self.amount_to_purchase_value.as_ref() else { + self.error_message = Some("Please enter a valid amount.".into()); + self.status = PurchaseTokensStatus::ErrorMessage("Invalid amount".into()); + self.confirmation_dialog = None; + return AppAction::None; + }; + + let Some(total_price_credits) = self.calculated_price_credits else { + self.error_message = + Some("Cannot calculate total price. Please fetch token pricing first.".into()); + self.status = PurchaseTokensStatus::ErrorMessage("No pricing fetched".into()); + self.confirmation_dialog = None; + return AppAction::None; + }; + + let Some(dialog) = self.confirmation_dialog.as_mut() else { + return AppAction::None; + }; + + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = PurchaseTokensStatus::WaitingForResult(now); + + AppAction::BackendTasks( + vec![ + BackendTask::TokenTask(Box::new(TokenTask::PurchaseTokens { + identity: self.identity_token_info.identity.clone(), + data_contract: Arc::new( + self.identity_token_info.data_contract.contract.clone(), + ), + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("Expected a key"), + amount: amount.value(), + total_agreed_price: total_price_credits, + })), + BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), + ], + BackendTasksExecutionMode::Sequential, + ) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } /// Renders a simple "Success!" screen after completion @@ -338,13 +376,12 @@ impl ScreenLike for PurchaseTokenScreen { fn refresh(&mut self) { // If you need to reload local identity data or re-check keys: - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities + if let Ok(all_identities) = self.app_context.load_local_user_identities() + && let Some(updated_identity) = all_identities .into_iter() .find(|id| id.identity.id() == self.identity_token_info.identity.identity.id()) - { - self.identity_token_info.identity = updated_identity; - } + { + self.identity_token_info.identity = updated_identity; } } @@ -474,12 +511,17 @@ impl ScreenLike for PurchaseTokenScreen { ui.add_space(10.0); - // Display calculated price - if let Some(calculated_price) = self.calculated_price { + // Display calculated price and total agreed price input + if let Some(calculated_price_credits) = self.calculated_price_credits { ui.group(|ui| { ui.heading("Calculated total price:"); - ui.label(format!("{} credits", calculated_price)); + let dash_amount = Amount::new(calculated_price_credits, DASH_DECIMAL_PLACES) + .with_unit_name("DASH"); + ui.label(format!("{} DASH ({} credits)",dash_amount, calculated_price_credits)); ui.label("Note: This is the calculated price based on the current pricing schedule."); + + ui.add_space(10.0); + }); } else if self.fetched_pricing_schedule.is_some() { ui.colored_label( @@ -494,9 +536,15 @@ impl ScreenLike for PurchaseTokenScreen { ui.separator(); ui.add_space(10.0); - // Purchase button (disabled if no pricing is available) - let can_purchase = - self.fetched_pricing_schedule.is_some() && self.calculated_price.is_some(); + // Purchase button (disabled if no valid amounts are available) + let can_purchase = self.fetched_pricing_schedule.is_some() + && self.calculated_price_credits.unwrap_or_default() > 0 + && self + .amount_to_purchase_value + .as_ref() + .map(|v| v.value()) + .unwrap_or_default() + > 0; let purchase_text = "Purchase".to_string(); if can_purchase { @@ -505,8 +553,30 @@ impl ScreenLike for PurchaseTokenScreen { .fill(Color32::from_rgb(0, 128, 255)) .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() && self.confirmation_dialog.is_none() { + if let (Some(amount), Some(total_price_credits)) = ( + self.amount_to_purchase_value.as_ref(), + self.calculated_price_credits, + ) { + let total_price_dash = + Amount::new(total_price_credits, DASH_DECIMAL_PLACES) + .with_unit_name("DASH"); + + self.confirmation_dialog = Some(ConfirmationDialog::new( + "Confirm Purchase".to_string(), + format!( + "Are you sure you want to purchase {} for {} ({} Credits)?", + amount, total_price_dash, total_price_credits + ), + )); + } else { + self.error_message = Some( + "Cannot calculate total price. Please fetch token pricing first." + .into(), + ); + self.status = + PurchaseTokensStatus::ErrorMessage("No pricing fetched".into()); + } } } else { let button = egui::Button::new( @@ -524,8 +594,8 @@ impl ScreenLike for PurchaseTokenScreen { ); } - // If the user pressed "Purchase," show a popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } @@ -589,3 +659,106 @@ impl ScreenWithWalletUnlock for PurchaseTokenScreen { self.error_message.as_ref() } } + +#[cfg(test)] +mod tests { + use crate::model::amount::DASH_DECIMAL_PLACES; + + #[test] + fn test_token_pricing_storage_and_calculation() { + // Test how prices should be stored and calculated + + // Case 1: Token with 8 decimals (like the user's case) + let token_decimals_8 = 8u8; + let user_price_per_token_dash = 0.001; // User wants 0.001 DASH per token + let user_price_per_token_credits = + (user_price_per_token_dash * 10f64.powi(DASH_DECIMAL_PLACES as i32)) as u64; + + println!("Test 1 - Token with 8 decimals, price 0.001 DASH per token:"); + println!( + " User enters: {} DASH per token", + user_price_per_token_dash + ); + println!( + " In credits: {} credits per token", + user_price_per_token_credits + ); + + // Platform expects price per smallest unit, not per token + let decimal_divisor_8 = 10u64.pow(token_decimals_8 as u32); + let platform_price_per_smallest_unit = user_price_per_token_credits / decimal_divisor_8; + + println!( + " Platform stores: {} credits per smallest unit", + platform_price_per_smallest_unit + ); + + // When buying 1 token (100,000,000 smallest units) + let tokens_to_buy = 1u64; + let amount_smallest_units = tokens_to_buy * 10u64.pow(token_decimals_8 as u32); + let total_price = amount_smallest_units * platform_price_per_smallest_unit; + + println!( + " Buying {} token ({} smallest units)", + tokens_to_buy, amount_smallest_units + ); + println!( + " Total: {} credits (should be {} credits for 0.001 DASH)", + total_price, user_price_per_token_credits + ); + + assert_eq!( + total_price, user_price_per_token_credits, + "Total should match expected price" + ); + + // Case 2: Token with 2 decimals + let token_decimals_2 = 2u8; + let user_price_2 = 0.1; // 0.1 DASH per token + let user_price_credits_2 = (user_price_2 * 10f64.powi(DASH_DECIMAL_PLACES as i32)) as u64; + + let divisor_2 = 10u64.pow(token_decimals_2 as u32); + let platform_price_2 = user_price_credits_2 / divisor_2; + + // Buy 5 tokens + let amount_2 = 5 * 10u64.pow(token_decimals_2 as u32); // 500 smallest units + let total_2 = amount_2 * platform_price_2; + + println!("\nTest 2 - Token with 2 decimals, 5 tokens at 0.1 DASH each:"); + println!( + " Platform price: {} credits per smallest unit", + platform_price_2 + ); + println!(" Total for 5 tokens: {} credits", total_2); + + assert_eq!( + total_2, + 5 * user_price_credits_2, + "Should be 0.5 DASH total" + ); + + // Case 3: Token with 0 decimals + let _token_decimals_0 = 0u8; + let user_price_0 = 0.05; // 0.05 DASH per token + let user_price_credits_0 = (user_price_0 * 10f64.powi(DASH_DECIMAL_PLACES as i32)) as u64; + + // With 0 decimals, price per token = price per smallest unit + let platform_price_0 = user_price_credits_0; // No division needed + + let amount_0 = 10; // 10 tokens = 10 smallest units (no decimals) + let total_0 = amount_0 * platform_price_0; + + println!("\nTest 3 - Token with 0 decimals, 10 tokens at 0.05 DASH each:"); + println!( + " Platform price: {} credits per smallest unit", + platform_price_0 + ); + println!(" Total for 10 tokens: {} credits", total_0); + + assert_eq!( + total_0, + 10 * user_price_credits_0, + "Should be 0.5 DASH total" + ); + } +} diff --git a/src/ui/tokens/freeze_tokens_screen.rs b/src/ui/tokens/freeze_tokens_screen.rs index 2562f3f6f..1fbc483b5 100644 --- a/src/ui/tokens/freeze_tokens_screen.rs +++ b/src/ui/tokens/freeze_tokens_screen.rs @@ -5,6 +5,9 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -52,6 +55,7 @@ pub struct FreezeTokensScreen { group: Option<(GroupContractPosition, Group)>, is_unilateral_group_member: bool, pub group_action_id: Option, + known_identities: Vec, /// The identity we want to freeze pub freeze_identity_id: String, @@ -62,8 +66,8 @@ pub struct FreezeTokensScreen { // Basic references pub app_context: Arc, - // Confirmation popup - show_confirmation_popup: bool, + // Confirmation dialog + confirmation_dialog: Option, // If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -73,6 +77,10 @@ pub struct FreezeTokensScreen { impl FreezeTokensScreen { pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let known_identities = app_context + .load_local_qualified_identities() + .expect("Identities not loaded"); + let possible_key = identity_token_info .identity .identity @@ -152,17 +160,17 @@ impl FreezeTokensScreen { }; let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } + if group.is_some() + && let Some((_, group)) = group.clone() + { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power + && your_power >= &group.required_power() + { + is_unilateral_group_member = true; } }; @@ -186,109 +194,110 @@ impl FreezeTokensScreen { status: FreezeTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, + known_identities, } } /// Renders text input for the identity to freeze fn render_freeze_identity_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Freeze Identity ID:"); - ui.text_edit_singleline(&mut self.freeze_identity_id); - }); + let _response = ui.add( + IdentitySelector::new( + "freeze_identity_selector", + &mut self.freeze_identity_id, + &self.known_identities, + ) + .label("Freeze Identity ID:") + .width(300.0), + ); } /// Confirmation popup fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Freeze") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Validate user input - let parsed = Identifier::from_string_try_encodings( - &self.freeze_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - if parsed.is_err() { - self.error_message = Some("Please enter a valid identity ID.".into()); - self.status = FreezeTokensStatus::ErrorMessage("Invalid identity".into()); - self.show_confirmation_popup = false; - return; - } - let freeze_id = parsed.unwrap(); - - ui.label(format!( - "Are you sure you want to freeze identity {}?", - self.freeze_identity_id - )); - - ui.add_space(10.0); + let msg = format!( + "Are you sure you want to freeze identity {}?", + self.freeze_identity_id + ); - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = FreezeTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch to backend - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::FreezeTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - freeze_identity: freeze_id, - group_info, - }, - ))); - } + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Freeze", msg) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + }); - // Cancel - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, + } + } - if !is_open { - self.show_confirmation_popup = false; + /// Handle confirmation OK action + fn confirmation_ok(&mut self) -> AppAction { + // Validate user input + let parsed = Identifier::from_string_try_encodings( + &self.freeze_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + if parsed.is_err() { + self.error_message = Some("Please enter a valid identity ID.".into()); + self.status = FreezeTokensStatus::ErrorMessage("Invalid identity".into()); + return AppAction::None; } - action + let freeze_id = parsed.unwrap(); + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = FreezeTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Dispatch to backend + AppAction::BackendTask(BackendTask::TokenTask(Box::new(TokenTask::FreezeTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + freeze_identity: freeze_id, + group_info, + }))) } /// Success screen @@ -356,13 +365,12 @@ impl ScreenLike for FreezeTokensScreen { fn refresh(&mut self) { // Reload identity if needed - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities + if let Ok(all_identities) = self.app_context.load_local_user_identities() + && let Some(updated_identity) = all_identities .into_iter() .find(|id| id.identity.id() == self.identity.identity.id()) - { - self.identity = updated_identity; - } + { + self.identity = updated_identity; } } @@ -549,12 +557,13 @@ impl ScreenLike for FreezeTokensScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + // Initialize confirmation dialog when button is clicked + self.confirmation_dialog = None; // Reset for fresh dialog } } - // If user pressed "Freeze," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/mint_tokens_screen.rs b/src/ui/tokens/mint_tokens_screen.rs index 8f9f8a424..a1ab3d629 100644 --- a/src/ui/tokens/mint_tokens_screen.rs +++ b/src/ui/tokens/mint_tokens_screen.rs @@ -3,7 +3,13 @@ use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; +use crate::model::amount::Amount; +use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::component_trait::{Component, ComponentResponse}; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -27,7 +33,6 @@ use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; -use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::{Identifier, IdentityPublicKey}; use eframe::egui::{self, Color32, Context, Ui}; use egui::RichText; @@ -52,10 +57,12 @@ pub struct MintTokensScreen { group: Option<(GroupContractPosition, Group)>, is_unilateral_group_member: bool, pub group_action_id: Option, + known_identities: Vec, pub recipient_identity_id: String, - pub amount_to_mint: String, + pub amount: Option, + pub amount_input: Option, status: MintTokensStatus, error_message: Option, @@ -63,7 +70,7 @@ pub struct MintTokensScreen { pub app_context: Arc, /// Confirmation popup - show_confirmation_popup: bool, + confirmation_dialog: Option, // If needed for password-based wallet unlocking: selected_wallet: Option>>, @@ -73,6 +80,10 @@ pub struct MintTokensScreen { impl MintTokensScreen { pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let known_identities = app_context + .load_local_qualified_identities() + .expect("Identities not loaded"); + let possible_key = identity_token_info .identity .identity @@ -152,17 +163,17 @@ impl MintTokensScreen { }; let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } + if group.is_some() + && let Some((_, group)) = group.clone() + { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power + && your_power >= &group.required_power() + { + is_unilateral_group_member = true; } }; @@ -181,150 +192,146 @@ impl MintTokensScreen { group, is_unilateral_group_member, group_action_id: None, + known_identities, recipient_identity_id: "".to_string(), - amount_to_mint: "".to_string(), + amount: None, + amount_input: None, status: MintTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, } } - /// Renders a text input for the user to specify an amount to mint + /// Renders an amount input for the user to specify an amount to mint fn render_amount_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Amount to Mint:"); - ui.text_edit_singleline(&mut self.amount_to_mint); - - // Since it's minting, we often don't do "Max." - // But you could show a help text or put constraints if needed. + // Lazy initialization with proper token configuration + let amount_input = self.amount_input.get_or_insert_with(|| { + // Create appropriate Amount based on token configuration + let token_amount = Amount::from_token(&self.identity_token_info, 0); + AmountInput::new(token_amount).with_label("Amount to Mint:") }); + + // Check if input should be disabled when operation is in progress + let enabled = match self.status { + MintTokensStatus::WaitingForResult(_) | MintTokensStatus::Complete => false, + MintTokensStatus::NotStarted | MintTokensStatus::ErrorMessage(_) => true, + }; + + let response = ui.add_enabled_ui(enabled, |ui| amount_input.show(ui)).inner; + + response.inner.update(&mut self.amount); + // errors are handled inside AmountInput } /// Renders an optional text input for the user to specify a "Recipient Identity" fn render_recipient_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Recipient:"); - ui.text_edit_singleline(&mut self.recipient_identity_id); - }); + let _response = ui.add( + IdentitySelector::new( + "mint_recipient_selector", + &mut self.recipient_identity_id, + &self.known_identities, + ) + .width(300.0) + .label("Recipient:") + .exclude(&[self.identity_token_info.identity.identity.id()]), + ); // If empty, minted tokens go to the 'issuer' identity (self.identity). } /// Renders a confirm popup with the final "Are you sure?" step fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Mint") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Validate user input - let amount_ok = self.amount_to_mint.parse::().ok(); - if amount_ok.is_none() { - self.error_message = Some("Please enter a valid amount.".into()); - self.status = MintTokensStatus::ErrorMessage("Invalid amount".into()); - self.show_confirmation_popup = false; - return; - } + let msg = format!( + "Are you sure you want to mint {} tokens to {}?", + self.amount.clone().unwrap_or(Amount::new(0, 0)), + self.recipient_identity_id + ); - let maybe_identifier = if self.recipient_identity_id.trim().is_empty() { - None - } else { - // Attempt to parse from base58 or hex - match Identifier::from_string_try_encodings( - &self.recipient_identity_id, - &[Encoding::Base58, Encoding::Hex], - ) { - Ok(id) => Some(id), - Err(_) => { - self.error_message = Some("Invalid recipient identity format.".into()); - self.status = - MintTokensStatus::ErrorMessage("Invalid recipient identity".into()); - self.show_confirmation_popup = false; - return; - } - } - }; + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Mint", msg) + .confirm_text(Some("Mint")) + .cancel_text(Some("Cancel")) + }); - ui.label(format!( - "Are you sure you want to mint {} token(s)?", - self.amount_to_mint - )); + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, + } + } - // If user provided a recipient: - if let Some(ref recipient_id) = maybe_identifier { - ui.label(format!( - "Recipient: {}", - recipient_id.to_string(Encoding::Base58) - )); - } else { - ui.label("No recipient specified; tokens will be minted to default identity."); - } + fn confirmation_ok(&mut self) -> AppAction { + if self.amount.is_none() || self.amount == Some(Amount::new(0, 0)) { + self.status = MintTokensStatus::ErrorMessage("Invalid amount".into()); + self.error_message = Some("Invalid amount".into()); + return AppAction::None; + } - ui.add_space(10.0); + let parsed_receiver_id = Identifier::from_string_try_encodings( + &self.recipient_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = MintTokensStatus::WaitingForResult(now); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch the actual backend mint action - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::MintTokens { - sending_identity: self.identity_token_info.identity.clone(), - data_contract: Arc::new( - self.identity_token_info.data_contract.contract.clone(), - ), - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - amount: amount_ok.unwrap(), - recipient_id: maybe_identifier, - group_info, - }, - ))); - } + if parsed_receiver_id.is_err() { + self.status = MintTokensStatus::ErrorMessage("Invalid receiver".into()); + self.error_message = Some("Invalid receiver".into()); + return AppAction::None; + } - // Cancel button - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let receiver_id = parsed_receiver_id.unwrap(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = MintTokensStatus::WaitingForResult(now); + + let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; - if !is_open { - self.show_confirmation_popup = false; - } - action + AppAction::BackendTask(BackendTask::TokenTask(Box::new(TokenTask::MintTokens { + sending_identity: self.identity_token_info.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + recipient_id: Some(receiver_id), + amount: self.amount.clone().unwrap_or(Amount::new(0, 0)).value(), + group_info, + }))) } - /// Renders a simple "Success!" screen after completion fn show_success_screen(&self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; @@ -391,13 +398,12 @@ impl ScreenLike for MintTokensScreen { fn refresh(&mut self) { // If you need to reload local identity data or re-check keys: - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities + if let Ok(all_identities) = self.app_context.load_local_user_identities() + && let Some(updated_identity) = all_identities .into_iter() .find(|id| id.identity.id() == self.identity_token_info.identity.identity.id()) - { - self.identity_token_info.identity = updated_identity; - } + { + self.identity_token_info.identity = updated_identity; } } @@ -543,7 +549,13 @@ impl ScreenLike for MintTokensScreen { "You are signing an existing group Mint so you are not allowed to choose the amount.", ); ui.add_space(5.0); - ui.label(format!("Amount: {}", self.amount_to_mint)); + ui.label(format!( + "Amount: {}", + self.amount + .as_ref() + .map(|a| a.to_string()) + .unwrap_or_default() + )); } else { self.render_amount_input(ui); } @@ -624,12 +636,21 @@ impl ScreenLike for MintTokensScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + let msg = format!( + "Are you sure you want to mint {} tokens to {}?", + self.amount.clone().unwrap_or(Amount::new(0, 0)), + self.recipient_identity_id + ); + self.confirmation_dialog = Some( + ConfirmationDialog::new("Confirm Mint", msg) + .confirm_text(Some("Mint")) + .cancel_text(Some("Cancel")), + ); } } // If the user pressed "Mint," show a popup - if self.show_confirmation_popup { + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/pause_tokens_screen.rs b/src/ui/tokens/pause_tokens_screen.rs index f6ed54f7d..c9333cfdf 100644 --- a/src/ui/tokens/pause_tokens_screen.rs +++ b/src/ui/tokens/pause_tokens_screen.rs @@ -5,6 +5,8 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -59,7 +61,7 @@ pub struct PauseTokensScreen { pub app_context: Arc, // Confirmation popup - show_confirmation_popup: bool, + confirmation_dialog: Option, // If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -148,17 +150,17 @@ impl PauseTokensScreen { }; let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } + if group.is_some() + && let Some((_, group)) = group.clone() + { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power + && your_power >= &group.required_power() + { + is_unilateral_group_member = true; } }; @@ -181,7 +183,7 @@ impl PauseTokensScreen { status: PauseTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -189,69 +191,61 @@ impl PauseTokensScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Pause") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label("Are you sure you want to pause token transfers for this contract?"); - ui.add_space(10.0); + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Pause".to_string(), + "Are you sure you want to pause token transfers for this contract?".to_string(), + ) + }); - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = PauseTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::PauseTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = PauseTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = + Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, }, - group_info, - }, - ))); - } - - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); - - if !is_open { - self.show_confirmation_popup = false; + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + AppAction::BackendTask(BackendTask::TokenTask(Box::new(TokenTask::PauseTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + group_info, + }))) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } fn show_success_screen(&self, ui: &mut Ui) -> AppAction { @@ -318,13 +312,12 @@ impl ScreenLike for PauseTokensScreen { } fn refresh(&mut self) { - if let Ok(all) = self.app_context.load_local_user_identities() { - if let Some(updated) = all + if let Ok(all) = self.app_context.load_local_user_identities() + && let Some(updated) = all .into_iter() .find(|id| id.identity.id() == self.identity.identity.id()) - { - self.identity = updated; - } + { + self.identity = updated; } } @@ -491,13 +484,17 @@ impl ScreenLike for PauseTokensScreen { .fill(Color32::from_rgb(0, 128, 255)) .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() && self.confirmation_dialog.is_none() { + self.confirmation_dialog = Some(ConfirmationDialog::new( + "Confirm Pause".to_string(), + "Are you sure you want to pause token transfers for this contract?" + .to_string(), + )); } } - // If user pressed "Pause," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/resume_tokens_screen.rs b/src/ui/tokens/resume_tokens_screen.rs index 5155985ab..dccec0693 100644 --- a/src/ui/tokens/resume_tokens_screen.rs +++ b/src/ui/tokens/resume_tokens_screen.rs @@ -5,6 +5,8 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -58,7 +60,7 @@ pub struct ResumeTokensScreen { pub app_context: Arc, // Confirmation popup - show_confirmation_popup: bool, + confirmation_dialog: Option, // If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -147,17 +149,17 @@ impl ResumeTokensScreen { }; let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } + if group.is_some() + && let Some((_, group)) = group.clone() + { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power + && your_power >= &group.required_power() + { + is_unilateral_group_member = true; } }; @@ -180,7 +182,7 @@ impl ResumeTokensScreen { status: ResumeTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -188,69 +190,62 @@ impl ResumeTokensScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Resume") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label("Are you sure you want to resume normal token actions for this contract?"); - ui.add_space(10.0); + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Resume".to_string(), + "Are you sure you want to resume normal token actions for this contract?" + .to_string(), + ) + }); - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = ResumeTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::ResumeTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = ResumeTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = + Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, }, - group_info, - }, - ))); - } - - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); - - if !is_open { - self.show_confirmation_popup = false; + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + AppAction::BackendTask(BackendTask::TokenTask(Box::new(TokenTask::ResumeTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + group_info, + }))) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } fn show_success_screen(&self, ui: &mut Ui) -> AppAction { @@ -317,13 +312,12 @@ impl ScreenLike for ResumeTokensScreen { } fn refresh(&mut self) { - if let Ok(all) = self.app_context.load_local_user_identities() { - if let Some(updated) = all + if let Ok(all) = self.app_context.load_local_user_identities() + && let Some(updated) = all .into_iter() .find(|id| id.identity.id() == self.identity.identity.id()) - { - self.identity = updated; - } + { + self.identity = updated; } } @@ -491,13 +485,16 @@ impl ScreenLike for ResumeTokensScreen { .fill(Color32::from_rgb(0, 128, 255)) .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() && self.confirmation_dialog.is_none() { + self.confirmation_dialog = Some(ConfirmationDialog::new( + "Confirm Resume".to_string(), + "Are you sure you want to resume normal token actions for this contract?".to_string(), + )); } } - // If user pressed "Resume," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/set_token_price_screen.rs b/src/ui/tokens/set_token_price_screen.rs index 374071c9b..36d699e94 100644 --- a/src/ui/tokens/set_token_price_screen.rs +++ b/src/ui/tokens/set_token_price_screen.rs @@ -3,7 +3,12 @@ use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; +use crate::model::amount::{Amount, DASH_DECIMAL_PLACES}; use crate::model::wallet::Wallet; +use crate::ui::components::ComponentResponse; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -20,6 +25,7 @@ use dash_sdk::dpp::data_contract::GroupContractPosition; use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; use dash_sdk::dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; use dash_sdk::dpp::data_contract::group::Group; @@ -44,6 +50,24 @@ pub enum PricingType { RemovePricing, } +impl From for PricingType { + fn from(schedule: TokenPricingSchedule) -> Self { + match schedule { + TokenPricingSchedule::SinglePrice(_) => PricingType::SinglePrice, + TokenPricingSchedule::SetPrices(_) => PricingType::TieredPricing, + } + } +} + +impl From> for PricingType { + fn from(schedule: Option) -> Self { + match schedule { + Some(schedule) => PricingType::from(schedule), + None => PricingType::RemovePricing, + } + } +} + /// Internal states for the mint process. #[derive(PartialEq)] pub enum SetTokenPriceStatus { @@ -63,9 +87,15 @@ pub struct SetTokenPriceScreen { pub group_action_id: Option, pub token_pricing_schedule: String, - pricing_type: PricingType, - single_price: String, - tiered_prices: Vec<(String, String)>, + /// Token pricing schedule to use; if None, we will remove the pricing schedule + pub pricing_type: PricingType, + + // AmountInput components for pricing - following the design pattern + single_price_amount: Option, + single_price_input: Option, + + // Tiered pricing with AmountInput components + pub tiered_prices: Vec<(Option, Option)>, // (amount_input, price_input) status: SetTokenPriceStatus, error_message: Option, @@ -74,6 +104,7 @@ pub struct SetTokenPriceScreen { /// Confirmation popup show_confirmation_popup: bool, + confirmation_dialog: Option, // If needed for password-based wallet unlocking: selected_wallet: Option>>, @@ -81,14 +112,50 @@ pub struct SetTokenPriceScreen { show_password: bool, } +/// 1 Dash = 100,000,000,000 credits +pub const CREDITS_PER_DASH: Credits = 100_000_000_000; + impl SetTokenPriceScreen { - /// Converts Dash amount to credits (1 Dash = 100,000,000,000 credits) - fn dash_to_credits(dash_amount: f64) -> Credits { - (dash_amount * 100_000_000_000.0) as Credits + fn token_decimal_divisor(&self) -> u64 { + 10u64.pow( + self.identity_token_info + .token_config + .conventions() + .decimals() as u32, + ) + } + + fn minimum_price_amount(&self) -> Amount { + Amount::new(self.token_decimal_divisor(), DASH_DECIMAL_PLACES).with_unit_name("DASH") + } + + fn validate_price_for_token(&self, price: &Amount) -> Result { + let credits_price_per_token = price.value(); + if credits_price_per_token == 0 { + return Err("Price must be greater than 0".to_string()); + } + + let decimal_divisor = self.token_decimal_divisor(); + + if credits_price_per_token < decimal_divisor { + return Err(format!( + "Price too low for this token's precision. Minimum price is {}.", + self.minimum_price_amount() + )); + } + + if credits_price_per_token % decimal_divisor != 0 { + return Err(format!( + "Price must be in multiples of {} to match the token decimals.", + self.minimum_price_amount() + )); + } + + Ok(credits_price_per_token / decimal_divisor) } pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { - let possible_key = identity_token_info + let possible_key: Option<&IdentityPublicKey> = identity_token_info .identity .identity .get_first_public_key_matching( @@ -169,17 +236,17 @@ impl SetTokenPriceScreen { }; let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } + if group.is_some() + && let Some((_, group)) = group.clone() + { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power + && your_power >= &group.required_power() + { + is_unilateral_group_member = true; } }; @@ -199,19 +266,72 @@ impl SetTokenPriceScreen { is_unilateral_group_member, group_action_id: None, token_pricing_schedule: "".to_string(), - pricing_type: PricingType::SinglePrice, - single_price: "".to_string(), - tiered_prices: vec![("1".to_string(), "".to_string())], + pricing_type: PricingType::RemovePricing, + single_price_amount: None, + single_price_input: None, + tiered_prices: vec![(None, None)], status: SetTokenPriceStatus::NotStarted, error_message: None, app_context: app_context.clone(), show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, } } + pub fn with_schedule(self, token_pricing_schedule: Option) -> Self { + let token_decimals = self + .identity_token_info + .token_config + .conventions() + .decimals(); + let decimal_multiplier = 10u64.pow(token_decimals as u32); + + let (single_price_amount, tiered_prices) = match &token_pricing_schedule { + Some(TokenPricingSchedule::SinglePrice(price_per_smallest_unit)) => { + // Convert price per smallest unit back to price per token for display + let price_per_token = price_per_smallest_unit * decimal_multiplier; + let amount = + Amount::new(price_per_token, DASH_DECIMAL_PLACES).with_unit_name("DASH"); + (Some(amount), vec![(None, None)]) + } + Some(TokenPricingSchedule::SetPrices(prices)) => { + let tiered_prices = prices + .iter() + .map(|(amount, price_per_smallest_unit)| { + // Create amount input for token threshold + let amount_input = AmountInput::new(Amount::from_token( + &self.identity_token_info, + *amount, + )) + .with_hint_text("Token amount threshold"); + + // Convert price per smallest unit back to price per token for display + let price_per_token = price_per_smallest_unit * decimal_multiplier; + let price = Amount::new(price_per_token, DASH_DECIMAL_PLACES) + .with_unit_name("DASH"); + let price_input = AmountInput::new(price) + .with_hint_text("Enter price in Dash") + .with_min_amount(Some(1)); + (Some(amount_input), Some(price_input)) + }) + .collect::>(); + + (None, tiered_prices) + } + None => (None, vec![(None, None)]), + }; + + Self { + pricing_type: PricingType::from(token_pricing_schedule), + single_price_amount, + tiered_prices, + ..self + } + } + /// Renders the pricing input UI fn render_pricing_input(&mut self, ui: &mut Ui) { // Radio buttons for pricing type @@ -238,30 +358,47 @@ impl SetTokenPriceScreen { match self.pricing_type { PricingType::SinglePrice => { ui.label("Set a fixed price per token:"); - ui.horizontal(|ui| { - ui.label("Price per token (Dash):"); - ui.text_edit_singleline(&mut self.single_price); + + if self.token_decimal_divisor() > 1 { + ui.colored_label( + Color32::DARK_RED, + format!( + "Prices must be multiples of {} to match this token's precision.", + self.minimum_price_amount() + ), + ); + } + + // Lazy initialization of AmountInput following the design pattern + let single_price_input = self.single_price_input.get_or_insert_with(|| { + let initial_amount = self + .single_price_amount + .as_ref() + .cloned() + .unwrap_or_else(|| Amount::new_dash(0.0)); + AmountInput::new(initial_amount) + .with_label("Price per token:") + .with_hint_text("Enter price in Dash") + .with_min_amount(Some(1)) // Minimum 1 credit (very small amount) }); - // Show preview - if !self.single_price.is_empty() { - if let Ok(price) = self.single_price.parse::() { - if price > 0.0 { - ui.add_space(5.0); - let credits = Self::dash_to_credits(price); - ui.colored_label( - Color32::DARK_GREEN, - format!("Price: {} Dash per token ({} credits)", price, credits), - ); - } else { - ui.colored_label(Color32::DARK_RED, "X Price must be greater than 0"); - } - } else { - ui.colored_label( - Color32::DARK_RED, - "X Invalid price - must be a positive number", - ); - } + let response = single_price_input.show(ui); + + // Update the domain data if there's a valid change + if response.inner.has_changed() && response.inner.is_valid() { + self.single_price_amount = response.inner.changed_value().clone(); + } + + // Show validation preview + if let Some(amount) = &self.single_price_amount + && amount.value() > 0 + { + ui.add_space(5.0); + let credits = amount.value(); + ui.colored_label( + Color32::DARK_GREEN, + format!("Price: {} per token ({} credits)", amount, credits), + ); } } PricingType::TieredPricing => { @@ -309,50 +446,44 @@ impl SetTokenPriceScreen { }); }) .body(|mut body| { - for (i, (amount, price)) in self.tiered_prices.iter_mut().enumerate() { - body.row(25.0, |mut row| { + for i in 0..self.tiered_prices.len() { + body.row(30.0, |mut row| { row.col(|ui| { if i == 0 { - // First tier is hardcoded to 1 token - ui.label("1"); - *amount = "1".to_string(); // Ensure it's always 1 + // First tier is hardcoded to 1 token - create AmountInput with value 1 + let amount_input = + self.tiered_prices[i].0.get_or_insert_with(|| { + AmountInput::new(Amount::from_token( + &self.identity_token_info, + 1, + )) + .with_hint_text("Token amount threshold") + }); + amount_input.show(ui); + // Make sure it's always 1 - we could disable editing or show as read-only } else { - let dark_mode = ui.ctx().style().visuals.dark_mode; - ui.add( - egui::TextEdit::singleline(amount) - .hint_text( - RichText::new("100").color(Color32::GRAY), - ) - .desired_width(100.0) - .text_color( - crate::ui::theme::DashColors::text_primary( - dark_mode, - ), - ) - .background_color( - crate::ui::theme::DashColors::input_background( - dark_mode, - ), - ), - ); + // Other tiers use AmountInput for token amounts + let amount_input = + self.tiered_prices[i].0.get_or_insert_with(|| { + AmountInput::new(Amount::from_token( + &self.identity_token_info, + 0, + )) + .with_hint_text("Token amount threshold") + }); + amount_input.show(ui); } }); row.col(|ui| { - let dark_mode = ui.ctx().style().visuals.dark_mode; - ui.add( - egui::TextEdit::singleline(price) - .hint_text(RichText::new("50").color(Color32::GRAY)) - .desired_width(120.0) - .text_color(crate::ui::theme::DashColors::text_primary( - dark_mode, - )) - .background_color( - crate::ui::theme::DashColors::input_background( - dark_mode, - ), - ), - ); - ui.label(" Dash"); + // Use AmountInput for price with lazy initialization + let price_input = + self.tiered_prices[i].1.get_or_insert_with(|| { + AmountInput::new(Amount::new_dash(0.0)) + .with_hint_text("Enter price in Dash") + .with_min_amount(Some(1)) // Minimum 1 credit + }); + + let _response = price_input.show(ui); }); row.col(|ui| { if can_remove && i > 0 && ui.small_button("X").clicked() { @@ -370,8 +501,8 @@ impl SetTokenPriceScreen { ui.add_space(10.0); ui.horizontal(|ui| { if ui.button("+ Add Tier").clicked() { - // Add empty tier - user will fill in values - self.tiered_prices.push(("".to_string(), "".to_string())); + // Add empty tier with lazy initialization + self.tiered_prices.push((None, None)); } }); @@ -390,19 +521,20 @@ impl SetTokenPriceScreen { let mut valid_tiers = Vec::new(); let mut has_errors = false; - for (amount_str, price_str) in &self.tiered_prices { - if amount_str.trim().is_empty() || price_str.trim().is_empty() { - continue; - } + for (amount_input, price_input) in &self.tiered_prices { + let Some(price) = price_input.as_ref().and_then(|input| input.current_value()) else { + continue; // Skip if no price input is available + }; - match (amount_str.parse::(), price_str.parse::()) { - (Ok(amount), Ok(price)) if price > 0.0 => { - valid_tiers.push((amount, price)); - } - _ => { - has_errors = true; - } - } + let Some(amount_value) = amount_input + .as_ref() + .and_then(|input| input.current_value()) + else { + has_errors = true; + continue; // Skip if amount is invalid + }; + + valid_tiers.push((amount_value, price)); } // Only show preview if there are valid tiers or errors @@ -410,7 +542,7 @@ impl SetTokenPriceScreen { ui.group(|ui| { // Sort tiers by amount if !valid_tiers.is_empty() { - valid_tiers.sort_by_key(|(amount, _)| *amount); + valid_tiers.sort_by_key(|(amount, _)| amount.value()); } if has_errors { @@ -420,12 +552,20 @@ impl SetTokenPriceScreen { if !valid_tiers.is_empty() { ui.colored_label(Color32::DARK_GREEN, "Pricing Structure:"); for (amount, price) in &valid_tiers { - let credits = Self::dash_to_credits(*price); + let credits = price.value(); ui.label(format!( - " - {} or more tokens: {} Dash each ({} credits)", + " - {} or more tokens: {} each ({} credits)", amount, price, credits )); } + + if self.token_decimal_divisor() > 1 { + ui.add_space(5.0); + ui.label(format!( + "Each tier price must be a multiple of {}.", + self.minimum_price_amount() + )); + } } }); } @@ -435,49 +575,36 @@ impl SetTokenPriceScreen { fn create_pricing_schedule(&self) -> Result, String> { match self.pricing_type { PricingType::RemovePricing => Ok(None), - PricingType::SinglePrice => { - if self.single_price.trim().is_empty() { - return Err("Please enter a price".to_string()); - } - match self.single_price.trim().parse::() { - Ok(dash_price) if dash_price > 0.0 => { - let credits_price = Self::dash_to_credits(dash_price); - Ok(Some(TokenPricingSchedule::SinglePrice(credits_price))) - } - Ok(_) => Err("Price must be greater than 0".to_string()), - Err(_) => Err("Invalid price - must be a positive number".to_string()), - } - } + PricingType::SinglePrice => match &self.single_price_amount { + Some(amount) => self + .validate_price_for_token(amount) + .map(|price| Some(TokenPricingSchedule::SinglePrice(price))), + None => Err("Please enter a price".to_string()), + }, PricingType::TieredPricing => { let mut map = std::collections::BTreeMap::new(); - for (amount_str, price_str) in &self.tiered_prices { - if amount_str.trim().is_empty() || price_str.trim().is_empty() { + for (amount_input, price_input) in &self.tiered_prices { + let Some(price) = price_input.as_ref().and_then(|input| input.current_value()) + else { continue; - } + }; - let amount = amount_str.trim().parse::().map_err(|_| { - format!( - "Invalid amount '{}' - must be a positive number", - amount_str.trim() - ) - })?; - let dash_price = price_str.trim().parse::().map_err(|_| { - format!( - "Invalid price '{}' - must be a positive number", - price_str.trim() - ) - })?; - - if dash_price <= 0.0 { - return Err(format!( - "Price '{}' must be greater than 0", - price_str.trim() - )); + let Some(amount_value) = amount_input + .as_ref() + .and_then(|input| input.current_value()) + else { + continue; + }; + + let amount = amount_value.value(); + if amount == 0 { + continue; } - let credits_price = Self::dash_to_credits(dash_price); - map.insert(amount, credits_price); + let price_per_smallest_unit = self.validate_price_for_token(&price)?; + + map.insert(amount, price_per_smallest_unit); } if map.is_empty() { @@ -489,127 +616,183 @@ impl SetTokenPriceScreen { } } - /// Renders a confirm popup with the final "Are you sure?" step - fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm SetPricingSchedule") - .collapsible(false) - .open(&mut is_open) - .frame( - egui::Frame::default() - .fill(Color32::from_rgb(245, 245, 245)) - .stroke(egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 200))) - .shadow(egui::epaint::Shadow::default()) - .inner_margin(egui::Margin::same(20)) - .corner_radius(egui::CornerRadius::same(8)), - ) - .show(ui.ctx(), |ui| { - // Validate user input - let token_pricing_schedule_opt = match self.create_pricing_schedule() { - Ok(schedule) => schedule, - Err(error) => { - self.error_message = Some(error.clone()); - self.status = SetTokenPriceStatus::ErrorMessage(error); - self.show_confirmation_popup = false; - return; - } - }; + /// Validate the current pricing configuration before showing confirmation dialog + fn validate_pricing_configuration(&self) -> Result<(), String> { + match self.pricing_type { + PricingType::RemovePricing => Ok(()), + PricingType::SinglePrice => match &self.single_price_amount { + Some(amount) => self.validate_price_for_token(amount).map(|_| ()), + None => Err("Please enter a price".to_string()), + }, + PricingType::TieredPricing => { + let mut valid_tiers = 0; - // Show confirmation message based on pricing type - match &self.pricing_type { - PricingType::RemovePricing => { - ui.colored_label( - Color32::from_rgb(180, 100, 0), - "WARNING: Are you sure you want to remove the pricing schedule?", - ); - ui.label("This will make the token unavailable for direct purchase."); - } - PricingType::SinglePrice => { - if let Ok(dash_price) = self.single_price.trim().parse::() { - ui.label(format!( - "Are you sure you want to set a fixed price of {} Dash per token?", - dash_price - )); - } - } - PricingType::TieredPricing => { - ui.label("Are you sure you want to set the following tiered pricing?"); - ui.add_space(5.0); - for (amount_str, price_str) in &self.tiered_prices { - if amount_str.trim().is_empty() || price_str.trim().is_empty() { - continue; - } - if let (Ok(amount), Ok(dash_price)) = ( - amount_str.trim().parse::(), - price_str.trim().parse::(), - ) { - ui.label(format!( - " - {} or more tokens: {} Dash each", - amount, dash_price - )); - } - } + for (amount_input, price_input) in &self.tiered_prices { + let Some(price) = price_input.as_ref().and_then(|input| input.current_value()) + else { + continue; + }; + + let Some(amount_value) = amount_input + .as_ref() + .and_then(|input| input.current_value()) + else { + continue; + }; + + if amount_value.value() == 0 { + continue; } + + self.validate_price_for_token(&price)?; + valid_tiers += 1; } - ui.add_space(10.0); + if valid_tiers == 0 { + return Err("Please add at least one valid pricing tier".to_string()); + } - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = SetTokenPriceStatus::WaitingForResult(now); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; + Ok(()) + } + } + } - // Dispatch the actual backend mint action - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::SetDirectPurchasePrice { - identity: self.identity_token_info.identity.clone(), - data_contract: Arc::new( - self.identity_token_info.data_contract.contract.clone(), - ), - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - token_pricing_schedule: token_pricing_schedule_opt, - group_info, - }, - ))); + /// Generate the confirmation message for the set price dialog + /// + /// ## Panics + /// + /// Panics if the pricing type is not set correctly or if the single price is not a valid number. + fn confirmation_message(&self) -> String { + match &self.pricing_type { + PricingType::RemovePricing => { + "WARNING: Are you sure you want to remove the pricing schedule? This will make the token unavailable for direct purchase.".to_string() + } + PricingType::SinglePrice => { + if let Some(amount) = &self.single_price_amount { + format!( + "Are you sure you want to set a fixed price of {} per token?", + amount + ) + } else { + "Are you sure you want to set the pricing schedule?".to_string() } + } + PricingType::TieredPricing => { + let mut message = "Are you sure you want to set the following tiered pricing?".to_string(); + for (amount_input, price_input) in &self.tiered_prices { + let Some(price) = price_input.as_ref().and_then(|input| input.current_value()) else { + continue; // Skip if no price input is available + }; + + let Some(amount_value) = amount_input + .as_ref() + .and_then(|input| input.current_value()) + else { + continue; + }; - // Cancel button - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; + message.push_str(&format!( + "\n - {} or more tokens: {} each", + amount_value, price + )); } - }); + message + } + } + } + + /// Handle the confirmation action when user clicks OK + fn confirmation_ok(&mut self) -> AppAction { + self.show_confirmation_popup = false; + self.confirmation_dialog = None; // Reset the dialog for next use + + // Validate user input and create pricing schedule + let token_pricing_schedule_opt = match self.create_pricing_schedule() { + Ok(schedule) => schedule, + Err(error) => { + // This should not happen if validation was done before opening dialog, + // but we handle it as a safety net + self.set_error_state(format!("Validation error: {}", error)); + return AppAction::None; + } + }; + + // Set waiting state + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = SetTokenPriceStatus::WaitingForResult(now); + + // Prepare group info + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Create and return the backend task + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::SetDirectPurchasePrice { + identity: self.identity_token_info.identity.clone(), + data_contract: Arc::new(self.identity_token_info.data_contract.contract.clone()), + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("Expected a key"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + token_pricing_schedule: token_pricing_schedule_opt, + group_info, + }, + ))) + } + + /// Handle the cancel action when user clicks Cancel or closes dialog + fn confirmation_cancel(&mut self) -> AppAction { + self.show_confirmation_popup = false; + self.confirmation_dialog = None; // Reset the dialog for next use + AppAction::None + } + + /// Set error state with the given message + fn set_error_state(&mut self, error: String) { + self.error_message = Some(error.clone()); + self.status = SetTokenPriceStatus::ErrorMessage(error); + } + + /// Renders a confirm popup with the final "Are you sure?" step + fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { + // Prepare values before borrowing + let confirmation_message = self.confirmation_message(); + let is_danger_mode = self.pricing_type == PricingType::RemovePricing; + + // Lazy initialization of the confirmation dialog + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm pricing schedule update", confirmation_message) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + .danger_mode(is_danger_mode) + }); - if !is_open { - self.show_confirmation_popup = false; + let response = confirmation_dialog.show(ui); + + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => self.confirmation_ok(), + Some(ConfirmationStatus::Canceled) => self.confirmation_cancel(), + None => AppAction::None, } - action } /// Renders a simple "Success!" screen after completion @@ -680,13 +863,12 @@ impl ScreenLike for SetTokenPriceScreen { fn refresh(&mut self) { // If you need to reload local identity data or re-check keys: - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities + if let Ok(all_identities) = self.app_context.load_local_user_identities() + && let Some(updated_identity) = all_identities .into_iter() .find(|id| id.identity.id() == self.identity_token_info.identity.identity.id()) - { - self.identity_token_info.identity = updated_identity; - } + { + self.identity_token_info.identity = updated_identity; } } @@ -911,25 +1093,10 @@ impl ScreenLike for SetTokenPriceScreen { }; // Set price button - let can_proceed = match self.pricing_type { - PricingType::RemovePricing => true, - PricingType::SinglePrice => { - if let Ok(price) = self.single_price.trim().parse::() { - price > 0.0 - } else { - false - } - }, - PricingType::TieredPricing => { - self.tiered_prices.iter().any(|(amount, price)| { - !amount.trim().is_empty() && !price.trim().is_empty() && - amount.trim().parse::().is_ok() && - if let Ok(p) = price.trim().parse::() { p > 0.0 } else { false } - }) - } - }; + let validation_result = self.validate_pricing_configuration(); + let button_active = validation_result.is_ok() && !matches!(self.status, SetTokenPriceStatus::WaitingForResult(_)); - let button_color = if can_proceed { + let button_color = if validation_result.is_ok() { Color32::from_rgb(0, 128, 255) } else { Color32::from_rgb(100, 100, 100) @@ -939,10 +1106,10 @@ impl ScreenLike for SetTokenPriceScreen { .fill(button_color) .corner_radius(3.0); - let button_response = ui.add_enabled(can_proceed, button); + let button_response = ui.add_enabled(button_active, button); - if !can_proceed { - button_response.on_hover_text("Please enter valid pricing information"); + if let Err(hover_message) = validation_result { + button_response.on_disabled_hover_text(hover_message); } else if button_response.clicked() { self.show_confirmation_popup = true; } diff --git a/src/ui/tokens/tokens_screen/contract_details.rs b/src/ui/tokens/tokens_screen/contract_details.rs index 64e2c8c5c..926471086 100644 --- a/src/ui/tokens/tokens_screen/contract_details.rs +++ b/src/ui/tokens/tokens_screen/contract_details.rs @@ -1,9 +1,9 @@ -use crate::app::AppAction; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; use crate::ui::tokens::tokens_screen::TokensScreen; +use crate::{app::AppAction, ui::theme::DashColors}; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::Identifier; -use egui::Ui; +use egui::{Frame, Margin, Ui}; impl TokensScreen { /// Renders details for the selected_contract_id. @@ -14,13 +14,28 @@ impl TokensScreen { ) -> AppAction { let mut action = AppAction::None; + let mut go_back = false; + ui.horizontal(|ui| { + if ui.button("Back to Search Results").clicked() { + go_back = true; + } + }); + + if go_back { + self.selected_contract_id = None; + self.contract_details_loading = false; + self.selected_contract_description = None; + self.selected_token_infos.clear(); + return action; + } + + ui.add_space(10.0); + // Show loading spinner if data is being fetched if self.contract_details_loading { - ui.vertical_centered(|ui| { - ui.add_space(50.0); - ui.heading("Loading contract details..."); - ui.add_space(20.0); - ui.add(egui::widgets::Spinner::default().size(50.0)); + ui.horizontal(|ui| { + ui.label("Loading contract details..."); + ui.add(egui::widgets::Spinner::default().color(DashColors::DASH_BLUE)); }); return action; } @@ -29,10 +44,10 @@ impl TokensScreen { ui.heading("Contract Description:"); ui.add_space(10.0); ui.label(description.description.clone()); + ui.add_space(10.0); + ui.separator(); } - ui.add_space(10.0); - ui.separator(); ui.add_space(10.0); ui.heading("Tokens:"); @@ -42,54 +57,52 @@ impl TokensScreen { .filter(|token| token.data_contract_id == *contract_id) .cloned() .collect::>(); + let visuals = ui.visuals().clone(); for token in token_infos { - if token.data_contract_id == *contract_id { - ui.add_space(10.0); - ui.heading(format!("• {}", token.token_name.clone())); - ui.add_space(10.0); - ui.label(format!( - "ID: {}", - token.token_id.to_string(Encoding::Base58) - )); - ui.label(format!( - "Description: {}", - token + ui.add_space(10.0); + Frame::group(ui.style()) + .stroke(visuals.widgets.noninteractive.bg_stroke) + .fill(visuals.extreme_bg_color) + .inner_margin(Margin::same(12)) + .show(ui, |ui| { + ui.heading(token.token_name.clone()); + ui.add_space(6.0); + ui.label(format!( + "ID: {}", + token.token_id.to_string(Encoding::Base58) + )); + let description = token .description .clone() - .unwrap_or("No description".to_string()) - )); - } + .unwrap_or_else(|| "No description".to_string()); + ui.label(format!("Description: {}", description)); - ui.add_space(10.0); + ui.add_space(12.0); - // Add button to add token to my tokens - ui.horizontal(|ui| { - if ui.button("Add to My Tokens").clicked() { - match self.add_token_to_tracked_tokens(token.clone()) { - Ok(internal_action) => { - // Add token to my tokens - action |= internal_action; + ui.horizontal(|ui| { + if ui.button("Add to My Tokens").clicked() { + match self.add_token_to_tracked_tokens(token.clone()) { + Ok(internal_action) => { + action |= internal_action; + } + Err(e) => { + self.set_error_message(Some(e)); + } + } } - Err(e) => { - self.set_error_message(Some(e)); + if ui.button("View schema").clicked() { + match serde_json::to_string_pretty(&token.token_configuration) { + Ok(schema) => { + self.show_json_popup = true; + self.json_popup_text = schema; + } + Err(e) => { + self.set_error_message(Some(e.to_string())); + } + } } - } - } - if ui.button("View schema").clicked() { - // Show a popup window with the schema - match serde_json::to_string_pretty(&token.token_configuration) { - Ok(schema) => { - self.show_json_popup = true; - self.json_popup_text = schema; - } - Err(e) => { - self.set_error_message(Some(e.to_string())); - } - } - } - }); - - ui.add_space(20.0); + }); + }); } action diff --git a/src/ui/tokens/tokens_screen/data_contract_json_pop_up.rs b/src/ui/tokens/tokens_screen/data_contract_json_pop_up.rs index 00ed9d364..80d5521ac 100644 --- a/src/ui/tokens/tokens_screen/data_contract_json_pop_up.rs +++ b/src/ui/tokens/tokens_screen/data_contract_json_pop_up.rs @@ -1,3 +1,4 @@ +use crate::ui::theme::{ComponentStyles, DashColors, Shape}; use crate::ui::tokens::tokens_screen::TokensScreen; use egui::Ui; @@ -6,18 +7,53 @@ impl TokensScreen { pub(super) fn render_data_contract_json_popup(&mut self, ui: &mut Ui) { if self.show_json_popup { let mut is_open = true; + + // Draw dark overlay behind the dialog for better visibility + let screen_rect = ui.ctx().screen_rect(); + let painter = ui.ctx().layer_painter(egui::LayerId::new( + egui::Order::Background, + egui::Id::new("json_popup_overlay"), + )); + painter.rect_filled( + screen_rect, + 0.0, + egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120), // Semi-transparent black overlay + ); + egui::Window::new("Data Contract JSON") .collapsible(false) .resizable(true) .max_height(600.0) .max_width(800.0) .scroll(true) + .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .open(&mut is_open) + .frame(egui::Frame { + inner_margin: egui::Margin::same(16), + outer_margin: egui::Margin::same(0), + corner_radius: egui::CornerRadius::same(8), + shadow: egui::epaint::Shadow { + offset: [0, 8], + blur: 16, + spread: 0, + color: egui::Color32::from_rgba_unmultiplied(0, 0, 0, 100), + }, + fill: ui.style().visuals.window_fill, + stroke: egui::Stroke::new( + 1.0, + egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30), + ), + }) .show(ui.ctx(), |ui| { // Display the JSON in a multiline text box - ui.add_space(4.0); - ui.label("Below is the data contract JSON:"); - ui.add_space(4.0); + let dark_mode = ui.ctx().style().visuals.dark_mode; + ui.add_space(10.0); + ui.label( + egui::RichText::new("Below is the data contract JSON:") + .strong() + .color(DashColors::text_primary(dark_mode)), + ); + ui.add_space(10.0); egui::Resize::default() .id_salt("json_resize_area_for_contract") @@ -32,12 +68,29 @@ impl TokensScreen { }); }); - ui.add_space(10.0); + ui.add_space(20.0); + + // Close button styled like ConfirmationDialog + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let close_button = egui::Button::new( + egui::RichText::new("Close") + .color(ComponentStyles::secondary_button_text()), + ) + .fill(ComponentStyles::secondary_button_fill()) + .stroke(ComponentStyles::secondary_button_stroke()) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) + .min_size(egui::Vec2::new(80.0, 32.0)); - // A button to close - if ui.button("Close").clicked() { - self.show_json_popup = false; - } + if ui + .add(close_button) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() + { + self.show_json_popup = false; + } + }); + }); }); // If the user closed the window via the "x" in the corner diff --git a/src/ui/tokens/tokens_screen/distributions.rs b/src/ui/tokens/tokens_screen/distributions.rs index 1ade18d73..907517af8 100644 --- a/src/ui/tokens/tokens_screen/distributions.rs +++ b/src/ui/tokens/tokens_screen/distributions.rs @@ -1,4 +1,4 @@ -use crate::ui::components::styled::ClickableCollapsingHeader; +use crate::ui::theme::DashColors; use crate::ui::tokens::tokens_screen::{ DistributionEntry, DistributionFunctionUI, IntervalTimeUnit, PerpetualDistributionIntervalTypeUI, TokenDistributionRecipientUI, TokensScreen, sanitize_i64, @@ -11,18 +11,41 @@ impl TokensScreen { pub(super) fn render_distributions(&mut self, context: &Context, ui: &mut egui::Ui) { ui.add_space(5.0); - ClickableCollapsingHeader::new("Distribution") - .id_salt("token_creator_distribution") - .default_open(false) - .open(if self.should_reset_collapsing_states { Some(false) } else { None }) - .show(ui, |ui| { + ui.horizontal(|ui| { + // +/- button + let button_text = if self.token_creator_distribution_expanded { + "−" + } else { + "+" + }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if button_response.clicked() { + self.token_creator_distribution_expanded = + !self.token_creator_distribution_expanded; + } + ui.label("Distribution"); + }); + + if self.token_creator_distribution_expanded { ui.add_space(3.0); - // PERPETUAL DISTRIBUTION SETTINGS - if ui.checkbox( - &mut self.enable_perpetual_distribution, - "Enable Perpetual Distribution", - ).clicked() { + ui.indent("distribution_section", |ui| { + // PERPETUAL DISTRIBUTION SETTINGS + if ui + .checkbox( + &mut self.enable_perpetual_distribution, + "Enable Perpetual Distribution", + ) + .clicked() + { self.perpetual_dist_type = PerpetualDistributionIntervalTypeUI::TimeBased; }; if self.enable_perpetual_distribution { @@ -60,13 +83,14 @@ impl TokensScreen { ui.label(" - Distributes every "); // Restrict input to digits only - let response = ui.add( - TextEdit::singleline(&mut self.perpetual_dist_interval_input) - ); + let response = ui.add(TextEdit::singleline( + &mut self.perpetual_dist_interval_input, + )); // Optionally filter out non-digit input if response.changed() { - self.perpetual_dist_interval_input.retain(|c| c.is_ascii_digit()); + self.perpetual_dist_interval_input + .retain(|c| c.is_ascii_digit()); } // Dropdown for selecting unit @@ -87,7 +111,9 @@ impl TokensScreen { ui.selectable_value( &mut self.perpetual_dist_interval_unit, unit.clone(), - unit.label_for_amount(&self.perpetual_dist_interval_input), + unit.label_for_amount( + &self.perpetual_dist_interval_input, + ), ); } }); @@ -160,6 +186,7 @@ impl TokensScreen { DistributionFunctionUI::InvertedLogarithmic, "InvertedLogarithmic", ); + // DistributionFunctionUI::Random is not supported }); let response = crate::ui::helpers::info_icon_button(ui, "Info about distribution types"); @@ -318,9 +345,15 @@ Emits tokens in fixed amounts for specific intervals. ui.image(texture); }); ui.add_space(10.0); - } else if let Some(image) = self.function_images.get(&self.perpetual_dist_function) { - let texture = context.load_texture(self.perpetual_dist_function.name(), image.clone(), Default::default()); - self.function_textures.insert(self.perpetual_dist_function.clone(), texture.clone()); + } else if let Some(image) = self.function_images.get(&self.perpetual_dist_function) + { + let texture = context.load_texture( + self.perpetual_dist_function.name(), + image.clone(), + Default::default(), + ); + self.function_textures + .insert(self.perpetual_dist_function.clone(), texture.clone()); ui.add_space(10.0); ui.horizontal(|ui| { ui.add_space(50.0); // Shift image right @@ -345,12 +378,16 @@ Emits tokens in fixed amounts for specific intervals. if response.changed() { sanitize_u64(&mut self.step_count_input); } - if !self.step_count_input.is_empty() { - if let Ok((perpetual_dist_interval_input, step_count_input)) = self.perpetual_dist_interval_input.parse::().and_then(|perpetual_dist_interval_input| self.step_count_input.parse::().map(|step_count_input| (perpetual_dist_interval_input, step_count_input))) { + if !self.step_count_input.is_empty() + && let Ok((perpetual_dist_interval_input, step_count_input)) = self + .perpetual_dist_interval_input + .parse::() + .and_then(|perpetual_dist_interval_input| self.step_count_input.parse::().map(|step_count_input| (perpetual_dist_interval_input, step_count_input))) { let text = match self.perpetual_dist_type { PerpetualDistributionIntervalTypeUI::None => "".to_string(), PerpetualDistributionIntervalTypeUI::BlockBased => { - let amount = perpetual_dist_interval_input * step_count_input; + let amount = + perpetual_dist_interval_input * step_count_input; if amount == 1 { "Every Block".to_string() } else { @@ -358,11 +395,18 @@ Emits tokens in fixed amounts for specific intervals. } } PerpetualDistributionIntervalTypeUI::TimeBased => { - let amount = perpetual_dist_interval_input * step_count_input; - format!("Every {} {}", amount, self.perpetual_dist_interval_unit.capitalized_label_for_num_amount(amount)) + let amount = + perpetual_dist_interval_input * step_count_input; + format!( + "Every {} {}", + amount, + self.perpetual_dist_interval_unit + .capitalized_label_for_num_amount(amount) + ) } PerpetualDistributionIntervalTypeUI::EpochBased => { - let amount = perpetual_dist_interval_input * step_count_input; + let amount = + perpetual_dist_interval_input * step_count_input; if amount == 1 { "Every Epoch Change".to_string() } else { @@ -373,12 +417,13 @@ Emits tokens in fixed amounts for specific intervals. ui.label(RichText::new(text).color(Color32::GRAY)); } - } }); ui.horizontal(|ui| { ui.label(" - Decrease per Interval Numerator (n < 65,536):"); - let response = ui.add(TextEdit::singleline(&mut self.decrease_per_interval_numerator_input)); + let response = ui.add(TextEdit::singleline( + &mut self.decrease_per_interval_numerator_input, + )); if response.changed() { sanitize_u64(&mut self.decrease_per_interval_numerator_input); self.decrease_per_interval_numerator_input.truncate(5); @@ -387,7 +432,9 @@ Emits tokens in fixed amounts for specific intervals. ui.horizontal(|ui| { ui.label(" - Decrease per Interval Denominator (d < 65,536):"); - let response = ui.add(TextEdit::singleline(&mut self.decrease_per_interval_denominator_input)); + let response = ui.add(TextEdit::singleline( + &mut self.decrease_per_interval_denominator_input, + )); if response.changed() { sanitize_u64(&mut self.decrease_per_interval_denominator_input); self.decrease_per_interval_denominator_input.truncate(5); @@ -397,8 +444,10 @@ Emits tokens in fixed amounts for specific intervals. ui.horizontal(|ui| { ui.label(" - Start Period Offset (i64, optional):"); let response = ui.add( - TextEdit::singleline(&mut self.step_decreasing_start_period_offset_input) - .hint_text("None"), + TextEdit::singleline( + &mut self.step_decreasing_start_period_offset_input, + ) + .hint_text("None"), ); if response.changed() { sanitize_i64(&mut self.step_decreasing_start_period_offset_input); @@ -407,7 +456,9 @@ Emits tokens in fixed amounts for specific intervals. ui.horizontal(|ui| { ui.label(" - Initial Token Emission Amount:"); - let response = ui.add(TextEdit::singleline(&mut self.step_decreasing_initial_emission_input)); + let response = ui.add(TextEdit::singleline( + &mut self.step_decreasing_initial_emission_input, + )); if response.changed() { sanitize_u64(&mut self.step_decreasing_initial_emission_input); } @@ -427,8 +478,10 @@ Emits tokens in fixed amounts for specific intervals. ui.horizontal(|ui| { ui.label(" - Maximum Interval Count (optional):"); let response = ui.add( - TextEdit::singleline(&mut self.step_decreasing_max_interval_count_input) - .hint_text("None"), + TextEdit::singleline( + &mut self.step_decreasing_max_interval_count_input, + ) + .hint_text("None"), ); if response.changed() { sanitize_u64(&mut self.step_decreasing_max_interval_count_input); @@ -468,8 +521,8 @@ Emits tokens in fixed amounts for specific intervals. sanitize_u64(&mut amount_str); } - if let Ok((perpetual_dist_interval_input, step_position)) = self.perpetual_dist_interval_input.parse::().and_then(|perpetual_dist_interval_input| steps_str.parse::().map(|step_count_input| (perpetual_dist_interval_input, step_count_input))) { - if let Ok(amount) = amount_str.parse::() { + if let Ok((perpetual_dist_interval_input, step_position)) = self.perpetual_dist_interval_input.parse::().and_then(|perpetual_dist_interval_input| steps_str.parse::().map(|step_count_input| (perpetual_dist_interval_input, step_count_input))) + && let Ok(amount) = amount_str.parse::() { let every_text = match self.perpetual_dist_type { PerpetualDistributionIntervalTypeUI::None => "".to_string(), PerpetualDistributionIntervalTypeUI::BlockBased => { @@ -540,9 +593,6 @@ Emits tokens in fixed amounts for specific intervals. ui.label(RichText::new(text).color(Color32::GRAY)); } - - } - // If remove is clicked, remove the step at index i // and *do not* increment i, because the next element // now “shifts” into this index. @@ -568,14 +618,16 @@ Emits tokens in fixed amounts for specific intervals. DistributionFunctionUI::Linear => { ui.horizontal(|ui| { ui.label(" - Slope Numerator (a, { -255 ≤ a ≤ 256 }):"); - let response = ui.add(TextEdit::singleline(&mut self.linear_int_a_input)); + let response = + ui.add(TextEdit::singleline(&mut self.linear_int_a_input)); if response.changed() { sanitize_i64(&mut self.linear_int_a_input); } }); ui.horizontal(|ui| { ui.label(" - Slope Divisor (d, u64):"); - let response = ui.add(TextEdit::singleline(&mut self.linear_int_d_input)); + let response = + ui.add(TextEdit::singleline(&mut self.linear_int_d_input)); if response.changed() { sanitize_u64(&mut self.linear_int_d_input); } @@ -592,7 +644,9 @@ Emits tokens in fixed amounts for specific intervals. }); ui.horizontal(|ui| { ui.label(" - Starting Amount (b, i64):"); - let response = ui.add(TextEdit::singleline(&mut self.linear_int_starting_amount_input)); + let response = ui.add(TextEdit::singleline( + &mut self.linear_int_starting_amount_input, + )); if response.changed() { sanitize_i64(&mut self.linear_int_starting_amount_input); } @@ -647,8 +701,7 @@ Emits tokens in fixed amounts for specific intervals. ui.horizontal(|ui| { ui.label(" - Start Period Offset (s, optional, u64):"); let response = ui.add( - TextEdit::singleline(&mut self.poly_int_s_input) - .hint_text("None"), + TextEdit::singleline(&mut self.poly_int_s_input).hint_text("None"), ); if response.changed() && !self.poly_int_s_input.trim().is_empty() { sanitize_u64(&mut self.poly_int_s_input); @@ -673,7 +726,9 @@ Emits tokens in fixed amounts for specific intervals. TextEdit::singleline(&mut self.poly_int_min_value_input) .hint_text("None"), ); - if response.changed() && !self.poly_int_min_value_input.trim().is_empty() { + if response.changed() + && !self.poly_int_min_value_input.trim().is_empty() + { sanitize_u64(&mut self.poly_int_min_value_input); } }); @@ -684,7 +739,9 @@ Emits tokens in fixed amounts for specific intervals. TextEdit::singleline(&mut self.poly_int_max_value_input) .hint_text("None"), ); - if response.changed() && !self.poly_int_max_value_input.trim().is_empty() { + if response.changed() + && !self.poly_int_max_value_input.trim().is_empty() + { sanitize_u64(&mut self.poly_int_max_value_input); } }); @@ -697,7 +754,9 @@ Emits tokens in fixed amounts for specific intervals. sanitize_u64(&mut self.exp_a_input); }); ui.horizontal(|ui| { - ui.label(" - Exponent Rate Numerator (m, { -8 ≤ m ≤ 8 ; m ≠ 0 }):"); + ui.label( + " - Exponent Rate Numerator (m, { -8 ≤ m ≤ 8 ; m ≠ 0 }):", + ); ui.text_edit_singleline(&mut self.exp_m_input); sanitize_i64(&mut self.exp_m_input); }); @@ -713,10 +772,8 @@ Emits tokens in fixed amounts for specific intervals. }); ui.horizontal(|ui| { ui.label(" - Start Period Offset (s, optional, u64):"); - let response = ui.add( - TextEdit::singleline(&mut self.exp_s_input) - .hint_text("None"), - ); + let response = ui + .add(TextEdit::singleline(&mut self.exp_s_input).hint_text("None")); if response.changed() && !self.exp_s_input.trim().is_empty() { sanitize_u64(&mut self.exp_s_input); } @@ -756,7 +813,9 @@ Emits tokens in fixed amounts for specific intervals. DistributionFunctionUI::Logarithmic => { ui.horizontal(|ui| { - ui.label(" - Scaling Factor (a, i64, { -32_766 ≤ a ≤ 32_767 }):"); + ui.label( + " - Scaling Factor (a, i64, { -32_766 ≤ a ≤ 32_767 }):", + ); ui.text_edit_singleline(&mut self.log_a_input); sanitize_i64(&mut self.log_a_input); }); @@ -781,10 +840,8 @@ Emits tokens in fixed amounts for specific intervals. ui.horizontal(|ui| { ui.label(" - Start Period Offset (s, optional, u64):"); - let response = ui.add( - TextEdit::singleline(&mut self.log_s_input) - .hint_text("None"), - ); + let response = ui + .add(TextEdit::singleline(&mut self.log_s_input).hint_text("None")); if response.changed() && !self.log_s_input.trim().is_empty() { sanitize_u64(&mut self.log_s_input); } @@ -827,7 +884,9 @@ Emits tokens in fixed amounts for specific intervals. DistributionFunctionUI::InvertedLogarithmic => { ui.horizontal(|ui| { - ui.label(" - Scaling Factor (a, i64, { -32_766 ≤ a ≤ 32_767 }):"); + ui.label( + " - Scaling Factor (a, i64, { -32_766 ≤ a ≤ 32_767 }):", + ); ui.text_edit_singleline(&mut self.inv_log_a_input); sanitize_i64(&mut self.inv_log_a_input); }); @@ -853,8 +912,7 @@ Emits tokens in fixed amounts for specific intervals. ui.horizontal(|ui| { ui.label(" - Start Period Offset (s, optional, u64):"); let response = ui.add( - TextEdit::singleline(&mut self.inv_log_s_input) - .hint_text("None"), + TextEdit::singleline(&mut self.inv_log_s_input).hint_text("None"), ); if response.changed() && !self.inv_log_s_input.trim().is_empty() { sanitize_u64(&mut self.inv_log_s_input); @@ -879,7 +937,8 @@ Emits tokens in fixed amounts for specific intervals. TextEdit::singleline(&mut self.inv_log_min_value_input) .hint_text("None"), ); - if response.changed() && !self.inv_log_min_value_input.trim().is_empty() { + if response.changed() && !self.inv_log_min_value_input.trim().is_empty() + { sanitize_u64(&mut self.inv_log_min_value_input); } }); @@ -890,7 +949,8 @@ Emits tokens in fixed amounts for specific intervals. TextEdit::singleline(&mut self.inv_log_max_value_input) .hint_text("None"), ); - if response.changed() && !self.inv_log_max_value_input.trim().is_empty() { + if response.changed() && !self.inv_log_max_value_input.trim().is_empty() + { sanitize_u64(&mut self.inv_log_max_value_input); } }); @@ -937,7 +997,14 @@ Emits tokens in fixed amounts for specific intervals. ui.horizontal(|ui| { ui.label(" "); - self.perpetual_distribution_rules.render_control_change_rules_ui(ui, &self.groups_ui,"Perpetual Distribution Rules", None); + self.perpetual_distribution_rules + .render_control_change_rules_ui( + ui, + &self.groups_ui, + "Perpetual Distribution Rules", + None, + &mut self.token_creator_perpetual_distribution_rules_expanded, + ); }); ui.add_space(5.0); @@ -1005,10 +1072,12 @@ Emits tokens in fixed amounts for specific intervals. ui.horizontal(|ui| { ui.label(" "); if ui.button("Add New Distribution Entry").clicked() { - self.pre_programmed_distributions.push(DistributionEntry::default()); + self.pre_programmed_distributions + .push(DistributionEntry::default()); } }); } - }); + }); + } } } diff --git a/src/ui/tokens/tokens_screen/groups.rs b/src/ui/tokens/tokens_screen/groups.rs index f413cb00b..adc4a4e70 100644 --- a/src/ui/tokens/tokens_screen/groups.rs +++ b/src/ui/tokens/tokens_screen/groups.rs @@ -1,4 +1,6 @@ -use crate::ui::components::styled::ClickableCollapsingHeader; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::ui::components::identity_selector::IdentitySelector; +use crate::ui::theme::DashColors; use crate::ui::tokens::tokens_screen::TokensScreen; use dash_sdk::dpp::data_contract::GroupContractPosition; use dash_sdk::dpp::data_contract::group::v0::GroupV0; @@ -7,7 +9,7 @@ use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::Identifier; use eframe::epaint::Color32; -use egui::ComboBox; +use egui::RichText; use std::collections::BTreeMap; #[derive(Default, Clone)] @@ -98,16 +100,33 @@ impl TokensScreen { pub fn render_groups(&mut self, ui: &mut egui::Ui) { ui.add_space(5.0); - ClickableCollapsingHeader::new("Groups") - .id_salt("token_creator_groups") - .open(if self.should_reset_collapsing_states { - Some(false) + ui.horizontal(|ui| { + // +/- button + let button_text = if self.token_creator_groups_expanded { + "−" } else { - None - }) - .show(ui, |ui| { + "+" + }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if button_response.clicked() { + self.token_creator_groups_expanded = !self.token_creator_groups_expanded; + } + ui.label("Groups"); + }); + + if self.token_creator_groups_expanded { ui.add_space(3.0); - ui.label("Define one or more groups for multi-party control of the contract."); + + ui.indent("groups_section", |ui| { + ui.label("Define one or more groups for multi-party control of the contract."); ui.add_space(2.0); // Add main group selection input @@ -123,10 +142,31 @@ impl TokensScreen { let last_group_position = self.groups_ui.len().saturating_sub(1); for (group_position, group_ui) in self.groups_ui.iter_mut().enumerate() { - ClickableCollapsingHeader::new(format!("Group {}", group_position)) - .id_salt(format!("group_header_{}", group_position)) - .default_open(true) - .show(ui, |ui| { + ui.horizontal(|ui| { + // +/- button for individual groups + let group_key = format!("group_{}", group_position); + let is_expanded = self.token_creator_groups_items_expanded.contains(&group_key); + let button_text = if is_expanded { "−" } else { "+" }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if button_response.clicked() { + if is_expanded { + self.token_creator_groups_items_expanded.remove(&group_key); + } else { + self.token_creator_groups_items_expanded.insert(group_key.clone()); + } + } + ui.label(format!("Group {}", group_position)); + }); + + if self.token_creator_groups_items_expanded.contains(&format!("group_{}", group_position)) { ui.add_space(3.0); ui.horizontal(|ui| { @@ -144,38 +184,32 @@ impl TokensScreen { ui.horizontal(|ui| { ui.label(format!("Member {}:", j + 1)); - ComboBox::from_id_salt(format!("member_identity_selector_{}", j)) - .width(200.0) - .selected_text( - self.identities - .get(&Identifier::from_string(&member.identity_str, Encoding::Base58).unwrap_or_default()) - .map(|q| q.display_string()) - .unwrap_or_else(|| member.identity_str.clone()), - ) - .show_ui(ui, |ui| { - for (identifier, qualified_identity) in self.identities.iter() { - let id_str = identifier.to_string(Encoding::Base58); - - // Prevent duplicates unless in developer mode - if !self.app_context.is_developer_mode() - && group_ui - .members - .iter() - .enumerate().any(|(i, m)| i != j && m.identity_str == id_str) - { - continue; - } - - if ui - .selectable_label(false, qualified_identity.display_string()) - .clicked() - { - member.identity_str = id_str.clone(); - } - } - }); - - ui.text_edit_singleline(&mut member.identity_str); + // Collect other member identities to exclude duplicates + let exclude_identities: Vec<_> = if !self.app_context.is_developer_mode() { + group_ui + .members + .iter() + .enumerate() + .filter_map(|(i, m)| if i != j && !m.identity_str.is_empty() { + let identifier = Identifier::from_string(&m.identity_str, Encoding::Base58).ok()?; + Some(identifier) + } else { + None + }) + .collect() + } else { + Vec::new() + }; + + let identities = self.identities.values().collect::>(); + // Use the reusable identity selector widget + let _identity_response = ui.add(IdentitySelector::new( + format!("member_identity_selector_{}", j), + &mut member.identity_str, + &identities, + ) + .width(200.0) + .exclude(&exclude_identities)); ui.label("Power (u32):"); ui.text_edit_singleline(&mut member.power_str); @@ -221,10 +255,10 @@ impl TokensScreen { group_to_remove = Some(group_position); } } - }); + } } - if let Some(group_to_remove) = group_to_remove{ + if let Some(group_to_remove) = group_to_remove { self.groups_ui.remove(group_to_remove); } @@ -232,15 +266,23 @@ impl TokensScreen { if ui.button("Add New Group").clicked() { self.groups_ui.push(GroupConfigUI { required_power_str: "2".to_owned(), - members: vec![GroupMemberUI { - identity_str: self.selected_identity.as_ref().map(|q| q.identity.id().to_string(Encoding::Base58)).unwrap_or_default(), - power_str: "1".to_string(), - }, GroupMemberUI { - identity_str: "".to_string(), - power_str: "1".to_string(), - }], + members: vec![ + GroupMemberUI { + identity_str: self + .selected_identity + .as_ref() + .map(|q| q.identity.id().to_string(Encoding::Base58)) + .unwrap_or_default(), + power_str: "1".to_string(), + }, + GroupMemberUI { + identity_str: "".to_string(), + power_str: "1".to_string(), + }, + ], }); } - }); + }); + } } } diff --git a/src/ui/tokens/tokens_screen/keyword_search.rs b/src/ui/tokens/tokens_screen/keyword_search.rs index 13ceea4ad..5af0a2f4a 100644 --- a/src/ui/tokens/tokens_screen/keyword_search.rs +++ b/src/ui/tokens/tokens_screen/keyword_search.rs @@ -2,6 +2,7 @@ use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::contract::ContractTask; use crate::backend_task::tokens::TokenTask; +use crate::ui::theme::DashColors; use crate::ui::tokens::tokens_screen::{ ContractDescriptionInfo, ContractSearchStatus, TokensScreen, }; @@ -9,7 +10,7 @@ use chrono::Utc; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use eframe::emath::Align; use eframe::epaint::Color32; -use egui::Ui; +use egui::{RichText, Ui}; use egui_extras::{Column, TableBuilder}; impl TokensScreen { @@ -105,27 +106,28 @@ impl TokensScreen { ui.label("No tokens match your keyword."); } else { action |= self.render_search_results_table(ui, &results); - } - - // Pagination controls - ui.horizontal(|ui| { - if self.search_current_page > 1 && ui.button("Previous").clicked() { - // Go to previous page - action = self.goto_previous_search_page(); - } - - if !(self.next_cursors.is_empty() && self.previous_cursors.is_empty()) { - ui.label(format!("Page {}", self.search_current_page)); - } - - if self.search_has_next_page && ui.button("Next").clicked() { - // Go to next page - action = self.goto_next_search_page(); + // Pagination controls + if self.search_has_next_page || self.search_current_page > 1 { + ui.horizontal(|ui| { + if self.search_current_page > 1 && ui.button("Previous").clicked() { + // Go to previous page + action |= self.goto_previous_search_page(); + } + + if !(self.next_cursors.is_empty() && self.previous_cursors.is_empty()) { + ui.label(format!("Page {}", self.search_current_page)); + } + + if self.search_has_next_page && ui.button("Next").clicked() { + // Go to next page + action |= self.goto_next_search_page(); + } + }); } - }); + } } ContractSearchStatus::ErrorMessage(e) => { - ui.colored_label(Color32::RED, format!("Error: {}", e)); + ui.colored_label(Color32::DARK_RED, format!("Error: {}", e)); } } @@ -139,6 +141,8 @@ impl TokensScreen { ) -> AppAction { let mut action = AppAction::None; + let dark_mode = ui.visuals().dark_mode; + egui::ScrollArea::both().show(ui, |ui| { ui.set_min_width(ui.available_width()); ui.set_max_width(ui.available_width()); @@ -152,13 +156,28 @@ impl TokensScreen { .column(Column::initial(80.0).resizable(true)) // Action .header(30.0, |mut header| { header.col(|ui| { - ui.label("Contract ID"); + ui.label( + RichText::new("Contract ID") + .strong() + .size(14.0) + .color(DashColors::text_primary(dark_mode)), + ); }); header.col(|ui| { - ui.label("Contract Description"); + ui.label( + RichText::new("Contract Description") + .strong() + .size(14.0) + .color(DashColors::text_primary(dark_mode)), + ); }); header.col(|ui| { - ui.label("Action"); + ui.label( + RichText::new("Action") + .strong() + .size(14.0) + .color(DashColors::text_primary(dark_mode)), + ); }); }) .body(|mut body| { @@ -168,7 +187,13 @@ impl TokensScreen { ui.label(contract.data_contract_id.to_string(Encoding::Base58)); }); row.col(|ui| { - ui.label(contract.description.clone()); + let description = if contract.description.trim().is_empty() { + "None".to_string() + } else { + contract.description.clone() + }; + + ui.label(description); }); row.col(|ui| { // Example "Add" button diff --git a/src/ui/tokens/tokens_screen/mod.rs b/src/ui/tokens/tokens_screen/mod.rs index 7eaac396b..1ae75df50 100644 --- a/src/ui/tokens/tokens_screen/mod.rs +++ b/src/ui/tokens/tokens_screen/mod.rs @@ -17,7 +17,7 @@ use std::sync::{Arc, Mutex, RwLock}; use serde_json; use chrono::{DateTime, Duration, Utc}; -use dash_sdk::dpp::balances::credits::TokenAmount; +use dash_sdk::dpp::balances::credits::{TokenAmount}; use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use dash_sdk::dpp::data_contract::associated_token::token_configuration::v0::{TokenConfigurationPresetFeatures, TokenConfigurationV0}; use dash_sdk::dpp::data_contract::associated_token::token_distribution_rules::v0::TokenDistributionRulesV0; @@ -56,13 +56,17 @@ use crate::backend_task::{BackendTask, NO_IDENTITIES_FOUND}; use crate::app::{AppAction, DesiredAppAction}; use crate::context::AppContext; +use crate::model::amount::Amount; use crate::model::qualified_identity::{IdentityType, QualifiedIdentity}; use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::{ClickableCollapsingHeader, island_central_panel}; +use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::components::{Component, ComponentResponse}; use crate::ui::{BackendTaskSuccessResult, MessageType, RootScreenType, ScreenLike, ScreenType}; const EXP_FORMULA_PNG: &[u8] = include_bytes!("../../../../assets/exp_function.png"); @@ -71,6 +75,8 @@ const LOG_FORMULA_PNG: &[u8] = include_bytes!("../../../../assets/log_function.p const LINEAR_FORMULA_PNG: &[u8] = include_bytes!("../../../../assets/linear_function.png"); const POLYNOMIAL_FORMULA_PNG: &[u8] = include_bytes!("../../../../assets/polynomial_function.png"); +const DEFAULT_DECIMALS: u8 = 8; + pub fn load_formula_image(bytes: &[u8]) -> ColorImage { let image = ImageReader::new(std::io::Cursor::new(bytes)) .with_guessed_format() @@ -242,14 +248,32 @@ impl ChangeControlRulesUI { current_groups: &[GroupConfigUI], action_name: &str, special_case_option: Option<&mut bool>, + is_expanded: &mut bool, ) { - ClickableCollapsingHeader::new(action_name) - .id_salt(format!("{}_header", action_name.replace(" ", "_").to_lowercase())) - .show(ui, |ui| { - egui::Grid::new("basic_token_info_grid") - .num_columns(2) - .spacing([16.0, 8.0]) // Horizontal, vertical spacing - .show(ui, |ui| { + ui.horizontal(|ui| { + // +/- button + let button_text = if *is_expanded { "−" } else { "+" }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(crate::ui::theme::DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if button_response.clicked() { + *is_expanded = !*is_expanded; + } + ui.label(action_name); + }); + + if *is_expanded { + ui.indent(format!("{}_content", action_name), |ui| { + egui::Grid::new(format!("{}_grid", action_name)) + .num_columns(2) + .spacing([16.0, 8.0]) // Horizontal, vertical spacing + .show(ui, |ui| { // Authorized action takers ui.horizontal(|ui| { ui.label("Authorized to perform action:"); @@ -438,8 +462,8 @@ impl ChangeControlRulesUI { ); ui.end_row(); - if let Some(special_case_option) = special_case_option { - if action_name == "Freeze" && self.rules.authorized_to_make_change != AuthorizedActionTakers::NoOne { + if let Some(special_case_option) = special_case_option + && action_name == "Freeze" && self.rules.authorized_to_make_change != AuthorizedActionTakers::NoOne { ui.horizontal(|ui| { ui.checkbox( special_case_option, @@ -455,9 +479,9 @@ impl ChangeControlRulesUI { }); ui.end_row(); } - } }); - }); + }); + } } #[allow(clippy::too_many_arguments)] @@ -471,14 +495,34 @@ impl ChangeControlRulesUI { new_tokens_destination_identity_rules: &mut ChangeControlRulesUI, new_tokens_destination_identity: &mut String, minting_allow_choosing_destination_rules: &mut ChangeControlRulesUI, + is_expanded: &mut bool, + new_tokens_destination_expanded: &mut bool, + minting_allow_choosing_expanded: &mut bool, ) { - ClickableCollapsingHeader::new("Manual Mint") - .id_salt("manual_mint_header") - .show(ui, |ui| { - egui::Grid::new("basic_token_info_grid") - .num_columns(2) - .spacing([16.0, 8.0]) // Horizontal, vertical spacing - .show(ui, |ui| { + ui.horizontal(|ui| { + // +/- button + let button_text = if *is_expanded { "−" } else { "+" }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(crate::ui::theme::DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if button_response.clicked() { + *is_expanded = !*is_expanded; + } + ui.label("Manual Mint"); + }); + + if *is_expanded { + ui.indent("manual_mint_content", |ui| { + egui::Grid::new("manual_mint_grid") + .num_columns(2) + .spacing([16.0, 8.0]) // Horizontal, vertical spacing + .show(ui, |ui| { // Authorized action takers ui.horizontal(|ui| { ui.label("Authorized to perform action:"); @@ -708,7 +752,7 @@ impl ChangeControlRulesUI { ui.text_edit_singleline(new_tokens_destination_identity); ui.end_row(); - new_tokens_destination_identity_rules.render_control_change_rules_ui(ui, current_groups,"New Tokens Destination Identity Rules", None); + new_tokens_destination_identity_rules.render_control_change_rules_ui(ui, current_groups,"New Tokens Destination Identity Rules", None, new_tokens_destination_expanded); } ui.end_row(); @@ -722,7 +766,7 @@ impl ChangeControlRulesUI { if *minting_allow_choosing_destination { ui.end_row(); - minting_allow_choosing_destination_rules.render_control_change_rules_ui(ui, current_groups, "Minting Allow Choosing Destination Rules", None); + minting_allow_choosing_destination_rules.render_control_change_rules_ui(ui, current_groups, "Minting Allow Choosing Destination Rules", None, minting_allow_choosing_expanded); } ui.end_row(); @@ -739,7 +783,8 @@ impl ChangeControlRulesUI { } } }); - }); + }); + } } pub fn extract_change_control_rules( @@ -747,29 +792,29 @@ impl ChangeControlRulesUI { action_name: &str, ) -> Result { // 1) Update self.rules.authorized_to_make_change if it’s Identity or Group - if let AuthorizedActionTakers::Identity(_) = self.rules.authorized_to_make_change { - if let Some(ref id_str) = self.authorized_identity { - let parsed = Identifier::from_string(id_str, Encoding::Base58).map_err(|_| { - format!( - "Invalid base58 identifier for {} authorized identity", - action_name - ) - })?; - self.rules.authorized_to_make_change = AuthorizedActionTakers::Identity(parsed); - } + if let AuthorizedActionTakers::Identity(_) = self.rules.authorized_to_make_change + && let Some(ref id_str) = self.authorized_identity + { + let parsed = Identifier::from_string(id_str, Encoding::Base58).map_err(|_| { + format!( + "Invalid base58 identifier for {} authorized identity", + action_name + ) + })?; + self.rules.authorized_to_make_change = AuthorizedActionTakers::Identity(parsed); } // 2) Update self.rules.admin_action_takers if it’s Identity or Group - if let AuthorizedActionTakers::Identity(_) = self.rules.admin_action_takers { - if let Some(ref id_str) = self.admin_identity { - let parsed = Identifier::from_string(id_str, Encoding::Base58).map_err(|_| { - format!( - "Invalid base58 identifier for {} admin identity", - action_name - ) - })?; - self.rules.admin_action_takers = AuthorizedActionTakers::Identity(parsed); - } + if let AuthorizedActionTakers::Identity(_) = self.rules.admin_action_takers + && let Some(ref id_str) = self.admin_identity + { + let parsed = Identifier::from_string(id_str, Encoding::Base58).map_err(|_| { + format!( + "Invalid base58 identifier for {} admin identity", + action_name + ) + })?; + self.rules.admin_action_takers = AuthorizedActionTakers::Identity(parsed); } // 3) Construct the ChangeControlRules @@ -956,6 +1001,29 @@ pub struct TokensScreen { pending_backend_task: Option, refreshing_status: RefreshingStatus, should_reset_collapsing_states: bool, + // Token Creator expanded sections + token_creator_advanced_expanded: bool, + token_creator_action_rules_expanded: bool, + token_creator_main_control_expanded: bool, + token_creator_distribution_expanded: bool, + token_creator_groups_expanded: bool, + token_creator_groups_items_expanded: std::collections::HashSet, + token_creator_document_schemas_expanded: bool, + // Individual action rules expanded states + token_creator_manual_mint_expanded: bool, + token_creator_manual_burn_expanded: bool, + token_creator_freeze_expanded: bool, + token_creator_unfreeze_expanded: bool, + token_creator_destroy_frozen_expanded: bool, + token_creator_emergency_action_expanded: bool, + token_creator_max_supply_change_expanded: bool, + token_creator_conventions_change_expanded: bool, + token_creator_marketplace_expanded: bool, + token_creator_direct_purchase_pricing_expanded: bool, + // Nested rules expanded states + token_creator_new_tokens_destination_expanded: bool, + token_creator_minting_allow_choosing_expanded: bool, + token_creator_perpetual_distribution_rules_expanded: bool, // Contract Search pub selected_contract_id: Option, @@ -980,8 +1048,10 @@ pub struct TokensScreen { // Remove token confirm_remove_identity_token_balance_popup: bool, identity_token_balance_to_remove: Option, + remove_identity_token_balance_confirmation_dialog: Option, confirm_remove_token_popup: bool, token_to_remove: Option, + remove_token_confirmation_dialog: Option, // Reward explanations reward_explanations: IndexMap, @@ -1005,11 +1075,14 @@ pub struct TokensScreen { token_description_input: String, should_capitalize_input: bool, decimals_input: String, - base_supply_input: String, - max_supply_input: String, + base_supply_amount: Option, + base_supply_input: Option, + max_supply_amount: Option, + max_supply_input: Option, start_as_paused_input: bool, main_control_group_input: String, show_token_creator_confirmation_popup: bool, + token_creator_confirmation_dialog: Option, token_creator_status: TokenCreatorStatus, token_creator_error_message: Option, show_advanced_keeps_history: bool, @@ -1065,9 +1138,9 @@ pub struct TokensScreen { // --- FixedAmount --- pub fixed_amount_input: String, - // --- Random --- - pub random_min_input: String, - pub random_max_input: String, + // --- Random --- - not supported + // pub random_min_input: String, + // pub random_max_input: String, // --- StepDecreasingAmount --- pub step_count_input: String, @@ -1331,8 +1404,10 @@ impl TokensScreen { // Remove token confirm_remove_identity_token_balance_popup: false, identity_token_balance_to_remove: None, + remove_identity_token_balance_confirmation_dialog: None, confirm_remove_token_popup: false, token_to_remove: None, + remove_token_confirmation_dialog: None, // Reward explanations reward_explanations: IndexMap::new(), @@ -1348,6 +1423,7 @@ impl TokensScreen { wallet_password: String::new(), show_password: false, show_token_creator_confirmation_popup: false, + token_creator_confirmation_dialog: None, token_creator_status: TokenCreatorStatus::NotStarted, token_creator_error_message: None, token_names_input: vec![( @@ -1359,11 +1435,11 @@ impl TokensScreen { contract_keywords_input: String::new(), token_description_input: String::new(), should_capitalize_input: true, - decimals_input: 0.to_string(), - base_supply_input: TokenConfigurationV0::default_most_restrictive() - .base_supply() - .to_string(), - max_supply_input: String::new(), + decimals_input: DEFAULT_DECIMALS.to_string(), + base_supply_amount: None, + base_supply_input: None, + max_supply_amount: None, + max_supply_input: None, start_as_paused_input: false, show_advanced_keeps_history: false, token_advanced_keeps_history: TokenKeepsHistoryRulesV0::default_for_keeping_all_history( @@ -1410,8 +1486,8 @@ impl TokensScreen { perpetual_dist_interval_unit: IntervalTimeUnit::Day, perpetual_dist_function: DistributionFunctionUI::FixedAmount, fixed_amount_input: String::new(), - random_min_input: String::new(), - random_max_input: String::new(), + // random_min_input: String::new(), + // random_max_input: String::new(), step_count_input: String::new(), decrease_per_interval_numerator_input: String::new(), decrease_per_interval_denominator_input: String::new(), @@ -1486,6 +1562,29 @@ impl TokensScreen { function_images, function_textures: BTreeMap::default(), should_reset_collapsing_states: false, + // Token Creator expanded sections + token_creator_advanced_expanded: false, + token_creator_action_rules_expanded: false, + token_creator_main_control_expanded: false, + token_creator_distribution_expanded: false, + token_creator_groups_expanded: false, + token_creator_groups_items_expanded: std::collections::HashSet::new(), + token_creator_document_schemas_expanded: false, + // Individual action rules expanded states + token_creator_manual_mint_expanded: false, + token_creator_manual_burn_expanded: false, + token_creator_freeze_expanded: false, + token_creator_unfreeze_expanded: false, + token_creator_destroy_frozen_expanded: false, + token_creator_emergency_action_expanded: false, + token_creator_max_supply_change_expanded: false, + token_creator_conventions_change_expanded: false, + token_creator_marketplace_expanded: false, + token_creator_direct_purchase_pricing_expanded: false, + // Nested rules expanded states + token_creator_new_tokens_destination_expanded: false, + token_creator_minting_allow_choosing_expanded: false, + token_creator_perpetual_distribution_rules_expanded: false, // Token adding status adding_token_start_time: None, @@ -1622,17 +1721,17 @@ impl TokensScreen { let response = tri_state(ui, &mut parent_state, "Keep history"); // propagate changes from parent to all children - if response.clicked() { - if let Some(val) = parent_state { - self.token_advanced_keeps_history.keeps_transfer_history = val; - self.token_advanced_keeps_history.keeps_freezing_history = val; - self.token_advanced_keeps_history.keeps_minting_history = val; - self.token_advanced_keeps_history.keeps_burning_history = val; - self.token_advanced_keeps_history - .keeps_direct_pricing_history = val; - self.token_advanced_keeps_history - .keeps_direct_purchase_history = val; - } + if response.clicked() + && let Some(val) = parent_state + { + self.token_advanced_keeps_history.keeps_transfer_history = val; + self.token_advanced_keeps_history.keeps_freezing_history = val; + self.token_advanced_keeps_history.keeps_minting_history = val; + self.token_advanced_keeps_history.keeps_burning_history = val; + self.token_advanced_keeps_history + .keeps_direct_pricing_history = val; + self.token_advanced_keeps_history + .keeps_direct_purchase_history = val; } ui.add_space(8.0); @@ -2108,9 +2207,11 @@ impl TokensScreen { )]; self.contract_keywords_input = "".to_string(); self.token_description_input = "".to_string(); - self.decimals_input = "8".to_string(); - self.base_supply_input = "100000".to_string(); - self.max_supply_input = "".to_string(); + self.decimals_input = DEFAULT_DECIMALS.to_string(); // + self.base_supply_input = None; + self.base_supply_amount = None; + self.max_supply_input = None; + self.max_supply_amount = None; self.start_as_paused_input = false; self.should_capitalize_input = true; self.token_advanced_keeps_history = @@ -2137,8 +2238,8 @@ impl TokensScreen { self.perpetual_dist_type = PerpetualDistributionIntervalTypeUI::None; self.perpetual_dist_interval_input = "".to_string(); self.fixed_amount_input = "".to_string(); - self.random_min_input = "".to_string(); - self.random_max_input = "".to_string(); + // self.random_min_input = "".to_string(); + // self.random_max_input = "".to_string(); self.step_count_input = "".to_string(); self.decrease_per_interval_numerator_input = "".to_string(); self.decrease_per_interval_denominator_input = "".to_string(); @@ -2294,20 +2395,28 @@ impl TokensScreen { } }; - let mut is_open = true; - - egui::Window::new("Confirm Stop Tracking Balance") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label(format!( + // Lazy initialization of the confirmation dialog + let confirmation_dialog = self + .remove_identity_token_balance_confirmation_dialog + .get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Stop Tracking Balance", + format!( "Are you sure you want to stop tracking the token \"{}\" for identity \"{}\"?", token_to_remove.token_alias, token_to_remove.identity_id.to_string(Encoding::Base58) - )); + ), + ) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + }); + + // Show the dialog and handle the response + let response = confirmation_dialog.show(ui).inner; - // Confirm button - if ui.button("Confirm").clicked() { + if let Some(status) = response.dialog_response { + match status { + ConfirmationStatus::Confirmed => { if let Err(e) = self .app_context .remove_token_balance(token_to_remove.token_id, token_to_remove.identity_id) @@ -2317,26 +2426,19 @@ impl TokensScreen { MessageType::Error, Utc::now(), )); - self.confirm_remove_identity_token_balance_popup = false; - self.identity_token_balance_to_remove = None; } else { - self.confirm_remove_identity_token_balance_popup = false; - self.identity_token_balance_to_remove = None; self.refresh(); - }; + } + self.confirm_remove_identity_token_balance_popup = false; + self.identity_token_balance_to_remove = None; + self.remove_identity_token_balance_confirmation_dialog = None; } - - // Cancel button - if ui.button("Cancel").clicked() { + ConfirmationStatus::Canceled => { self.confirm_remove_identity_token_balance_popup = false; self.identity_token_balance_to_remove = None; + self.remove_identity_token_balance_confirmation_dialog = None; } - }); - - // If user closes the popup window (the [x] button), also reset state - if !is_open { - self.confirm_remove_identity_token_balance_popup = false; - self.identity_token_balance_to_remove = None; + } } } @@ -2357,49 +2459,89 @@ impl TokensScreen { .map(|t| t.token_name.clone()) .unwrap_or_else(|| token_to_remove.to_string(Encoding::Base58)); - let mut is_open = true; - - egui::Window::new("Confirm Remove Token") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label(format!( + // Lazy initialization of the confirmation dialog + let confirmation_dialog = self.remove_token_confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Remove Token", + format!( "Are you sure you want to stop tracking the token \"{}\"? You can re-add it later. Your actual token balance will not change with this action.", token_name, - )); - - // Confirm button - if ui.button("Confirm").clicked() { - if let Err(e) = self.app_context.db.remove_token( - &token_to_remove, - &self.app_context, - ) { + ), + ) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + }); + + // Show the dialog and handle the response + let response = confirmation_dialog.show(ui).inner; + + if let Some(status) = response.dialog_response { + match status { + ConfirmationStatus::Confirmed => { + if let Err(e) = self + .app_context + .db + .remove_token(&token_to_remove, &self.app_context) + { self.backend_message = Some(( format!("Error removing token balance: {}", e), MessageType::Error, Utc::now(), )); - self.confirm_remove_token_popup = false; - self.token_to_remove = None; } else { - self.confirm_remove_token_popup = false; - self.token_to_remove = None; self.refresh(); } + self.confirm_remove_token_popup = false; + self.token_to_remove = None; + self.remove_token_confirmation_dialog = None; } - - // Cancel button - if ui.button("Cancel").clicked() { + ConfirmationStatus::Canceled => { self.confirm_remove_token_popup = false; self.token_to_remove = None; + self.remove_token_confirmation_dialog = None; } - }); + } + } + } - // If user closes the popup window (the [x] button), also reset state - if !is_open { - self.confirm_remove_token_popup = false; - self.token_to_remove = None; + /// Renders the base supply amount input using AmountInput component + fn render_base_supply_input(&mut self, ui: &mut egui::Ui) { + let decimals = self.decimals_input.parse::().unwrap_or(0); + let input = self + .base_supply_input + .get_or_insert_with(|| AmountInput::new(Amount::new(0, decimals))); + + if decimals != input.decimal_places() { + // Update decimals; it will change actual value but I guess this is what user expects + input.set_decimal_places(decimals); + } + + let response = input.show(ui); + response.inner.update(&mut self.base_supply_amount); + } + + /// Renders the max supply amount input using AmountInput component + fn render_max_supply_input(&mut self, ui: &mut egui::Ui) { + let decimals = self.decimals_input.parse::().unwrap_or(0); + + let input = self.max_supply_input.get_or_insert_with(|| { + let initial_amount = Amount::new( + TokenConfigurationV0::default_most_restrictive() + .max_supply() + .unwrap_or(0), + decimals, + ); + + AmountInput::new(initial_amount) + }); + + if decimals != input.decimal_places() { + // Update decimals; it will change actual value but I guess this is what user expects + input.set_decimal_places(decimals); } + + let response = input.show(ui); + response.inner.update(&mut self.max_supply_amount); } } @@ -2422,6 +2564,11 @@ impl ScreenLike for TokensScreen { .map(|qi| (qi.identity.id(), qi)) .collect(); + // Clear pricing data to force re-fetching when tokens are selected + // This ensures we get updated pricing after changes like SetPrice + self.token_pricing_data.clear(); + self.pricing_loading_state.clear(); + self.my_tokens = my_tokens( &self.app_context, &self.identities, @@ -2458,6 +2605,11 @@ impl ScreenLike for TokensScreen { .map(|qi| (qi.identity.id(), qi)) .collect(); + // Clear pricing data to force re-fetching when tokens are selected + // This ensures we get updated pricing after changes like SetPrice + self.token_pricing_data.clear(); + self.pricing_loading_state.clear(); + self.my_tokens = my_tokens( &self.app_context, &self.identities, @@ -2693,10 +2845,10 @@ impl ScreenLike for TokensScreen { } } - if action == AppAction::None { - if let Some(bt) = self.pending_backend_task.take() { - action = AppAction::BackendTask(bt); - } + if action == AppAction::None + && let Some(bt) = self.pending_backend_task.take() + { + action = AppAction::BackendTask(bt); } action } @@ -2716,8 +2868,6 @@ impl ScreenLike for TokensScreen { { self.token_creator_status = TokenCreatorStatus::ErrorMessage(msg.to_string()); self.token_creator_error_message = Some(msg.to_string()); - } else { - return; } } TokensSubscreen::MyTokens => { @@ -2744,13 +2894,12 @@ impl ScreenLike for TokensScreen { } } TokensSubscreen::SearchTokens => { - if msg.contains("Error fetching tokens") { + if msg_type == MessageType::Error { self.contract_search_status = ContractSearchStatus::ErrorMessage(msg.to_string()); // Clear adding status on error self.adding_token_start_time = None; self.adding_token_name = None; - self.backend_message = Some((msg.to_string(), msg_type, Utc::now())); } else if msg.contains("Added token") | msg.contains("Token already added") | msg.contains("Saved token to db") @@ -2763,8 +2912,6 @@ impl ScreenLike for TokensScreen { MessageType::Success, Utc::now(), )); - } else { - return; } } } @@ -2858,6 +3005,7 @@ impl ScreenWithWalletUnlock for TokensScreen { mod tests { use std::path::Path; + use crate::app_dir::copy_env_file_if_not_exists; use crate::database::Database; use crate::model::qualified_identity::IdentityStatus; use crate::model::qualified_identity::encrypted_key_storage::KeyStorage; @@ -2896,6 +3044,7 @@ mod tests { let db = Arc::new(Database::new(db_file_path).unwrap()); db.initialize(Path::new(&db_file_path)).unwrap(); + copy_env_file_if_not_exists(); // Required by AppContext::new() let app_context = AppContext::new(Network::Regtest, db, None, Default::default()) .expect("Expected to create AppContext"); let mut token_creator_ui = TokensScreen::new(&app_context, TokensSubscreen::TokenCreator); @@ -2924,6 +3073,7 @@ mod tests { wallet_index: None, top_ups: BTreeMap::new(), status: IdentityStatus::Active, + network: Network::Dash, }; token_creator_ui.selected_identity = Some(mock_identity); @@ -2939,9 +3089,11 @@ mod tests { TokenNameLanguage::English, true, )]; - token_creator_ui.base_supply_input = "5000000".to_string(); - token_creator_ui.max_supply_input = "10000000".to_string(); - token_creator_ui.decimals_input = "8".to_string(); + token_creator_ui.base_supply_input = None; + token_creator_ui.base_supply_amount = Some(Amount::new(5000000, 8)); + token_creator_ui.max_supply_input = None; + token_creator_ui.max_supply_amount = Some(Amount::new(10000000, 8)); + token_creator_ui.decimals_input = DEFAULT_DECIMALS.to_string(); token_creator_ui.start_as_paused_input = true; token_creator_ui.token_advanced_keeps_history = TokenKeepsHistoryRulesV0::default_for_keeping_all_history(true); @@ -3009,7 +3161,7 @@ mod tests { // ------------------------------------------------- // Groups // ------------------------------------------------- - // We'll define 2 groups for testing: positions 2 (main) and 7 + // We'll define 2 groups for testing: positions 0 (main) and 1 token_creator_ui.groups_ui = vec![ GroupConfigUI { required_power_str: "2".to_string(), @@ -3169,25 +3321,26 @@ mod tests { }; assert_eq!( new_dest_id.to_string(Encoding::Base58), - "GCMnPwQZcH3RP9atgkmvtmN45QrVcYvh5cmUYARHBTu9" + "BCMnPwQZcH3RP9atgkmvtmN45QrVcYvh5cmUYARHBTu9" ); assert!(dist_rules_v0.minting_allow_choosing_destination); // F) Check the Groups - // (Positions 2 and 7, from above) + // (Positions 0 and 1, from above) assert_eq!(contract_v1.groups.len(), 2, "We added two groups in the UI"); - let group2 = contract_v1.groups.get(&2).expect("Expected group pos=2"); + + let group0 = contract_v1.groups.get(&0).expect("Expected group pos=0"); assert_eq!( - group2.required_power(), + group0.required_power(), 2, - "Group #2 required_power mismatch" + "Group #0 required_power mismatch" ); - let members = &group2.members(); + let members = &group0.members(); assert_eq!(members.len(), 2); - let group7 = contract_v1.groups.get(&7).expect("Expected group pos=7"); - assert_eq!(group7.required_power(), 1); - assert_eq!(group7.members().len(), 0); + let group1 = contract_v1.groups.get(&1).expect("Expected group pos=1"); + assert_eq!(group1.required_power(), 1); + assert_eq!(group1.members().len(), 0); } #[test] @@ -3196,6 +3349,7 @@ mod tests { let db = Arc::new(Database::new(db_file_path).unwrap()); db.initialize(Path::new(&db_file_path)).unwrap(); + copy_env_file_if_not_exists(); // required by AppContext::new() let app_context = AppContext::new(Network::Regtest, db, None, Default::default()) .expect("Expected to create AppContext"); let mut token_creator_ui = TokensScreen::new(&app_context, TokensSubscreen::TokenCreator); @@ -3224,6 +3378,7 @@ mod tests { wallet_index: None, top_ups: BTreeMap::new(), status: IdentityStatus::Active, + network: Network::Dash, }; token_creator_ui.selected_identity = Some(mock_identity); @@ -3239,12 +3394,16 @@ mod tests { true, )]; + // Set base supply + token_creator_ui.base_supply_amount = Some(Amount::new(1000000, 8)); + // Enable perpetual distribution, select Random token_creator_ui.enable_perpetual_distribution = true; token_creator_ui.perpetual_dist_type = PerpetualDistributionIntervalTypeUI::TimeBased; - token_creator_ui.perpetual_dist_interval_input = "60000".to_string(); - token_creator_ui.random_min_input = "100".to_string(); - token_creator_ui.random_max_input = "200".to_string(); + token_creator_ui.perpetual_dist_function = DistributionFunctionUI::FixedAmount; + token_creator_ui.perpetual_dist_interval_input = "60".to_string(); + token_creator_ui.perpetual_dist_interval_unit = IntervalTimeUnit::Second; + token_creator_ui.fixed_amount_input = "100".to_string(); // Parse + build let build_args = token_creator_ui @@ -3293,11 +3452,10 @@ mod tests { RewardDistributionType::TimeBasedDistribution { interval, function } => { assert_eq!(*interval, 60000, "Expected 60s (in ms)"); match function { - DistributionFunction::Random { min, max } => { - assert_eq!(*min, 100); - assert_eq!(*max, 200); + DistributionFunction::FixedAmount { amount } => { + assert_eq!(*amount, 100); } - _ => panic!("Expected DistributionFunction::Random"), + _ => panic!("Expected DistributionFunction::FixedAmount"), } } _ => panic!("Expected TimeBasedDistribution"), @@ -3310,6 +3468,7 @@ mod tests { let db = Arc::new(Database::new(db_file_path).unwrap()); db.initialize(Path::new(&db_file_path)).unwrap(); + copy_env_file_if_not_exists(); // required by AppContext::new() let app_context = AppContext::new(Network::Regtest, db, None, Default::default()) .expect("Expected to create AppContext"); let mut token_creator_ui = TokensScreen::new(&app_context, TokensSubscreen::TokenCreator); @@ -3338,6 +3497,7 @@ mod tests { wallet_index: None, top_ups: BTreeMap::new(), status: IdentityStatus::Active, + network: Network::Dash, }; token_creator_ui.selected_identity = Some(mock_identity); diff --git a/src/ui/tokens/tokens_screen/my_tokens.rs b/src/ui/tokens/tokens_screen/my_tokens.rs index 6ec08d4b7..cc50d5555 100644 --- a/src/ui/tokens/tokens_screen/my_tokens.rs +++ b/src/ui/tokens/tokens_screen/my_tokens.rs @@ -1,6 +1,7 @@ use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; +use crate::model::amount::Amount; use crate::ui::Screen; use crate::ui::components::styled::{ClickableCollapsingHeader, StyledButton}; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; @@ -423,8 +424,10 @@ impl TokensScreen { }); row.col(|ui| { if let Some(balance) = itb.balance { - let formatted_balance = balance.to_string(); - ui.label(formatted_balance); + // Create an amount using the token's decimal places and alias + let decimals = itb.token_config.conventions().decimals(); + let amount = Amount::new(balance, decimals).with_unit_name(&itb.token_alias); + ui.label(amount.to_string_without_unit()); } else if ui.button("Check").clicked() { action = AppAction::BackendTask(BackendTask::TokenTask(Box::new(TokenTask::QueryIdentityTokenBalance(itb.clone().into())))); } @@ -434,8 +437,10 @@ impl TokensScreen { if itb.available_actions.can_estimate { if let Some(known_rewards) = itb.estimated_unclaimed_rewards { ui.horizontal(|ui| { - let formatted_rewards = known_rewards.to_string(); - ui.label(formatted_rewards); + // Create an amount for rewards using the token's decimal places and alias + let decimals = itb.token_config.conventions().decimals(); + let rewards_amount = Amount::new(known_rewards, decimals); + ui.label(rewards_amount.to_string()); // Info button to show explanation let identity_token_id = IdentityTokenIdentifier { @@ -473,29 +478,27 @@ impl TokensScreen { }); } row.col(|ui| { - ui.horizontal(|ui| { - if itb.available_actions.shown_buttons() < 6 { - action |= self.render_actions(itb, &token_info, 0..10, ui); - } else { - action |= self.render_actions(itb, &token_info, 0..3, ui); - // Expandable advanced actions menu - ui.menu_button("...", |ui| { - action |= self.render_actions(itb, &token_info, 3..128, ui); - }); - } + if itb.available_actions.shown_buttons() < 3 { + action |= self.render_actions(itb, &token_info, 0..10, ui); + } else { + action |= self.render_actions(itb, &token_info, 0..3, ui); + // Expandable advanced actions menu + ui.menu_button("...", |ui| { + action |= self.render_actions(itb, &token_info, 3..128, ui); + }); + } - // Remove - if ui - .button("X") - .on_hover_text( - "Remove identity token balance from DET", - ) - .clicked() - { - self.confirm_remove_identity_token_balance_popup = true; - self.identity_token_balance_to_remove = Some(itb.into()); - } - }); + // Remove + if ui + .button("X") + .on_hover_text( + "Remove identity token balance from DET", + ) + .clicked() + { + self.confirm_remove_identity_token_balance_popup = true; + self.identity_token_balance_to_remove = Some(itb.into()); + } }); }); } @@ -516,12 +519,17 @@ impl TokensScreen { egui::ScrollArea::vertical().show(ui, |ui| { ui.heading("Reward Estimation Details"); ui.separator(); + let decimal_places = + token_info.token_configuration.conventions().decimals(); + let unit_name = token_info + .token_configuration + .conventions() + .plural_form_by_language_code_or_default("en"); + let reward_amount = + Amount::new(explanation.total_amount, decimal_places) + .with_unit_name(unit_name); - let formatted_total = explanation.total_amount.to_string(); - ui.label(format!( - "Total Estimated Rewards: {} tokens", - formatted_total - )); + ui.label(format!("Total Estimated Rewards: {}", reward_amount)); ui.separator(); ClickableCollapsingHeader::new("Basic Explanation") @@ -600,64 +608,62 @@ impl TokensScreen { ) -> AppAction { let mut pos = 0; let mut action = AppAction::None; - - ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - ui.add_space(-9.0); - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 5.0; - - if range.contains(&pos) { - if itb.available_actions.can_transfer { - if let Some(balance) = itb.balance { - // Transfer - if ui.button("Transfer").clicked() { - action = AppAction::AddScreen(Screen::TransferTokensScreen( - TransferTokensScreen::new( - itb.to_token_balance(balance), - &self.app_context, - ), - )); - } + ui.spacing_mut().item_spacing.x = 5.0; + + if range.contains(&pos) { + if itb.available_actions.can_transfer { + if let Some(balance) = itb.balance { + // Transfer + if ui.button("Transfer").clicked() { + action = AppAction::AddScreen(Screen::TransferTokensScreen( + TransferTokensScreen::new( + itb.to_token_balance(balance), + &self.app_context, + ), + )); } - } else { - // Disabled, grayed-out Transfer button - ui.add_enabled( - false, - egui::Button::new(RichText::new("Transfer").color(Color32::GRAY)), - ) - .on_hover_text("Transfer not available"); } + } else { + // Disabled, grayed-out Transfer button + ui.add_enabled( + false, + egui::Button::new(RichText::new("Transfer").color(Color32::GRAY)), + ) + .on_hover_text("Transfer not available"); } + } - pos += 1; + pos += 1; - // Claim - if itb.available_actions.can_claim { - if range.contains(&pos) && ui.button("Claim").clicked() { - match self.app_context.get_contract_by_token_id(&itb.token_id) { - Ok(Some(contract)) => { - action = AppAction::AddScreen(Screen::ClaimTokensScreen(ClaimTokensScreen::new( + // Claim + if itb.available_actions.can_claim { + if range.contains(&pos) && ui.button("Claim").clicked() { + match self.app_context.get_contract_by_token_id(&itb.token_id) { + Ok(Some(contract)) => { + action = AppAction::AddScreen(Screen::ClaimTokensScreen( + ClaimTokensScreen::new( itb.into(), contract, token_info.token_configuration.clone(), &self.app_context, - ))); - ui.close_menu(); - } - Ok(None) => { - self.set_error_message(Some("Token contract not found".to_string())); - } - Err(e) => { - self.set_error_message(Some(format!("Error fetching token contract: {e}"))); - } + ), + )); + ui.close_kind(egui::UiKind::Menu); + } + Ok(None) => { + self.set_error_message(Some("Token contract not found".to_string())); + } + Err(e) => { + self.set_error_message(Some(format!("Error fetching token contract: {e}"))); } } - pos += 1; } + pos += 1; + } - if itb.available_actions.can_mint { - if range.contains(&pos) && ui.button("Mint").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + if itb.available_actions.can_mint { + if range.contains(&pos) && ui.button("Mint").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::MintTokensScreen( @@ -673,13 +679,13 @@ impl TokensScreen { } }; - ui.close_menu(); - } - pos += 1; + ui.close_kind(egui::UiKind::Menu); } - if itb.available_actions.can_burn { - if range.contains(&pos) && ui.button("Burn").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + pos += 1; + } + if itb.available_actions.can_burn { + if range.contains(&pos) && ui.button("Burn").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::BurnTokensScreen( @@ -694,13 +700,13 @@ impl TokensScreen { self.set_error_message(Some(e)); } }; - ui.close_menu(); - } - pos += 1; + ui.close_kind(egui::UiKind::Menu); } - if itb.available_actions.can_freeze { - if range.contains(&pos) && ui.button("Freeze").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + pos += 1; + } + if itb.available_actions.can_freeze { + if range.contains(&pos) && ui.button("Freeze").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::FreezeTokensScreen( @@ -715,13 +721,13 @@ impl TokensScreen { self.set_error_message(Some(e)); } }; - ui.close_menu(); - } - pos += 1; + ui.close_kind(egui::UiKind::Menu); } - if itb.available_actions.can_destroy { - if range.contains(&pos) && ui.button("Destroy Frozen Identity Tokens").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + pos += 1; + } + if itb.available_actions.can_destroy { + if range.contains(&pos) && ui.button("Destroy Frozen Identity Tokens").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::DestroyFrozenFundsScreen( @@ -736,13 +742,13 @@ impl TokensScreen { self.set_error_message(Some(e)); } }; - ui.close_menu(); - } - pos += 1; + ui.close_kind(egui::UiKind::Menu); } - if itb.available_actions.can_unfreeze { - if range.contains(&pos) && ui.button("Unfreeze").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + pos += 1; + } + if itb.available_actions.can_unfreeze { + if range.contains(&pos) && ui.button("Unfreeze").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::UnfreezeTokensScreen( @@ -757,14 +763,14 @@ impl TokensScreen { self.set_error_message(Some(e)); } }; - ui.close_menu(); - } - pos += 1; + ui.close_kind(egui::UiKind::Menu); } - if itb.available_actions.can_do_emergency_action { - if range.contains(&pos) { - if ui.button("Pause").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + pos += 1; + } + if itb.available_actions.can_do_emergency_action { + if range.contains(&pos) { + if ui.button("Pause").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::PauseTokensScreen( @@ -779,14 +785,14 @@ impl TokensScreen { self.set_error_message(Some(e)); } }; - ui.close_menu(); - } - pos += 1; + ui.close_kind(egui::UiKind::Menu); } + pos += 1; + } - if range.contains(&pos) { - if ui.button("Resume").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + if range.contains(&pos) { + if ui.button("Resume").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::ResumeTokensScreen( @@ -801,23 +807,23 @@ impl TokensScreen { self.set_error_message(Some(e)); } }; - ui.close_menu(); - } - pos += 1; - } - } - if itb.available_actions.can_claim { - if range.contains(&pos) && ui.button("View Claims").clicked() { - action = AppAction::AddScreen(Screen::ViewTokenClaimsScreen( - ViewTokenClaimsScreen::new(itb.into(), &self.app_context), - )); - ui.close_menu(); + ui.close_kind(egui::UiKind::Menu); } pos += 1; } - if itb.available_actions.can_update_config { - if range.contains(&pos) && ui.button("Update Config").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + } + if itb.available_actions.can_claim { + if range.contains(&pos) && ui.button("View Claims").clicked() { + action = AppAction::AddScreen(Screen::ViewTokenClaimsScreen( + ViewTokenClaimsScreen::new(itb.into(), &self.app_context), + )); + ui.close_kind(egui::UiKind::Menu); + } + pos += 1; + } + if itb.available_actions.can_update_config { + if range.contains(&pos) && ui.button("Update Config").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::UpdateTokenConfigScreen(Box::new( @@ -832,50 +838,50 @@ impl TokensScreen { self.set_error_message(Some(e)); } }; - ui.close_menu(); - } - pos += 1; + ui.close_kind(egui::UiKind::Menu); } - if itb.available_actions.can_maybe_purchase { - if range.contains(&pos) { - // Check if we have pricing data - let has_pricing_data = self.token_pricing_data.contains_key(&itb.token_id); - let is_loading = self - .pricing_loading_state - .get(&itb.token_id) - .copied() + pos += 1; + } + if itb.available_actions.can_maybe_purchase { + if range.contains(&pos) { + // Check if we have pricing data + let has_pricing_data = self.token_pricing_data.contains_key(&itb.token_id); + let is_loading = self + .pricing_loading_state + .get(&itb.token_id) + .copied() + .unwrap_or(false); + + if is_loading { + // Show loading spinner + ui.add(egui::Spinner::new()); + } else if has_pricing_data { + // Check if identity has enough credits for at least one token + let has_credits = self + .app_context + .get_identity_by_id(&itb.identity_id) + .map(|identity_opt| { + identity_opt + .map(|identity| { + use dash_sdk::dpp::identity::accessors::IdentityGettersV0; + // Check if identity has enough credits for the minimum token price + if let Some(Some(pricing)) = + self.token_pricing_data.get(&itb.token_id) + { + let min_price = get_min_token_price(pricing); + identity.identity.balance() >= min_price + } else { + false + } + }) + .unwrap_or(false) + }) .unwrap_or(false); - if is_loading { - // Show loading spinner - ui.add(egui::Spinner::new()); - } else if has_pricing_data { - // Check if identity has enough credits for at least one token - let has_credits = self - .app_context - .get_identity_by_id(&itb.identity_id) - .map(|identity_opt| { - identity_opt - .map(|identity| { - use dash_sdk::dpp::identity::accessors::IdentityGettersV0; - // Check if identity has enough credits for the minimum token price - if let Some(Some(pricing)) = - self.token_pricing_data.get(&itb.token_id) - { - let min_price = get_min_token_price(pricing); - identity.identity.balance() >= min_price - } else { - false - } - }) - .unwrap_or(false) - }) - .unwrap_or(false); - - if has_credits { - // Purchase button enabled - if ui.button("Purchase").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + if has_credits { + // Purchase button enabled + if ui.button("Purchase").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::PurchaseTokenScreen( @@ -890,11 +896,11 @@ impl TokensScreen { self.set_error_message(Some(e)); } }; - ui.close_menu(); - } - } else { - // Disabled, grayed-out Purchase button - ui.add_enabled( + ui.close_kind(egui::UiKind::Menu); + } + } else { + // Disabled, grayed-out Purchase button + ui.add_enabled( false, egui::Button::new(RichText::new("Purchase").color(egui::Color32::GRAY)), ) @@ -906,15 +912,15 @@ impl TokensScreen { "No credits available for purchase".to_string() } }); - } } } - pos += 1; } - if itb.available_actions.can_set_price && range.contains(&pos) { - // Set Price - if ui.button("Set Price").clicked() { - match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { + pos += 1; + } + if itb.available_actions.can_set_price && range.contains(&pos) { + // Set Price + if ui.button("Set Price").clicked() { + match IdentityTokenInfo::try_from_identity_token_maybe_balance_with_actions_with_lookup(itb, &self.app_context) { Ok(info) => { action = AppAction::AddScreen( Screen::SetTokenPriceScreen( @@ -930,13 +936,9 @@ impl TokensScreen { } }; - ui.close_menu(); - } + ui.close_kind(egui::UiKind::Menu); } - }); - - }); - + } action } @@ -1015,21 +1017,15 @@ impl TokensScreen { self.show_token_info_popup = Some(*token_id); } - ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - ui.add_space(-1.0); - - ui.horizontal(|ui| { - // Remove button - if ui - .button("X") - .on_hover_text("Remove token from DET") - .clicked() - { - self.confirm_remove_token_popup = true; - self.token_to_remove = Some(*token_id); - } - }); - }); + // Remove button + if ui + .button("X") + .on_hover_text("Remove token from DET") + .clicked() + { + self.confirm_remove_token_popup = true; + self.token_to_remove = Some(*token_id); + } }); }); } diff --git a/src/ui/tokens/tokens_screen/token_creator.rs b/src/ui/tokens/tokens_screen/token_creator.rs index 602f4af6f..1e2644a55 100644 --- a/src/ui/tokens/tokens_screen/token_creator.rs +++ b/src/ui/tokens/tokens_screen/token_creator.rs @@ -11,12 +11,15 @@ use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::Identifier; use eframe::epaint::Color32; -use egui::{ComboBox, Context, RichText, TextEdit, Ui}; +use egui::{ComboBox, Context, RichText, TextEdit, Ui}; +use crate::ui::theme::DashColors; use crate::app::{AppAction, BackendTasksExecutionMode}; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; -use crate::ui::components::styled::{StyledCheckbox, ClickableCollapsingHeader}; +use crate::ui::components::styled::{StyledCheckbox}; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::Component; use crate::ui::helpers::{add_identity_key_chooser, TransactionType}; use crate::ui::tokens::tokens_screen::{TokenBuildArgs, TokenCreatorStatus, TokenNameLanguage, TokensScreen, ChangeControlRulesUI}; @@ -245,13 +248,15 @@ impl TokensScreen { } // Row 2: Base Supply + // We put label manually to comply with grid layout; + // errors will be rendered in second column ui.label("Base Supply*:"); - ui.text_edit_singleline(&mut self.base_supply_input); + self.render_base_supply_input(ui); ui.end_row(); // Row 3: Max Supply ui.label("Max Supply:"); - ui.text_edit_singleline(&mut self.max_supply_input); + self.render_max_supply_input(ui); ui.end_row(); // Row 4: Contract Keywords @@ -292,104 +297,133 @@ impl TokensScreen { ui.add_space(10.0); // 5) Advanced settings toggle - ClickableCollapsingHeader::new("Advanced") - .id_salt("token_creator_advanced") - .default_open(false) - .open(if self.should_reset_collapsing_states { Some(false) } else { None }) - .show(ui, |ui| { - ui.add_space(3.0); - - // Use `Grid` to align labels and text edits - egui::Grid::new("advanced_token_info_grid") - .num_columns(2) - .spacing([16.0, 8.0]) // Horizontal, vertical spacing - .show(ui, |ui| { - - // Start as paused - ui.horizontal(|ui| { - StyledCheckbox::new(&mut self.start_as_paused_input, "Start as paused").show(ui); - - crate::ui::helpers::info_icon_button(ui, "When enabled, the token will be created in a paused state, meaning transfers will be disabled by default. All other token features—such as distributions and manual minting—remain fully functional. To allow transfers in the future, the token must be unpaused via an emergency action. It is strongly recommended to enable emergency actions if this option is selected, unless the intention is to permanently disable transfers."); - }); - ui.end_row(); + ui.horizontal(|ui| { + // +/- button + let button_text = if self.token_creator_advanced_expanded { "−" } else { "+" }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if button_response.clicked() { + self.token_creator_advanced_expanded = !self.token_creator_advanced_expanded; + } + ui.label("Advanced"); + }); - self.history_row(ui); - ui.end_row(); + if self.token_creator_advanced_expanded { + ui.add_space(3.0); - // Name should be capitalized - ui.horizontal(|ui| { - StyledCheckbox::new(&mut self.should_capitalize_input, "Name should be capitalized").show(ui); + ui.indent("advanced_section", |ui| { + // Use `Grid` to align labels and text edits + egui::Grid::new("advanced_token_info_grid") + .num_columns(2) + .spacing([16.0, 8.0]) // Horizontal, vertical spacing + .show(ui, |ui| { + // Start as paused + ui.horizontal(|ui| { + StyledCheckbox::new(&mut self.start_as_paused_input, "Start as paused").show(ui); + crate::ui::helpers::info_icon_button(ui, "When enabled, the token will be created in a paused state, meaning transfers will be disabled by default. All other token features—such as distributions and manual minting—remain fully functional. To allow transfers in the future, the token must be unpaused via an emergency action. It is strongly recommended to enable emergency actions if this option is selected, unless the intention is to permanently disable transfers."); + }); + ui.end_row(); - crate::ui::helpers::info_icon_button(ui, "This is used only as helper information to client applications that will use token. This informs them on whether to capitalize the token name or not by default."); - }); - ui.end_row(); + self.history_row(ui); + ui.end_row(); - // Decimals - ui.horizontal(|ui| { - ui.label("Max Decimals:"); - // Restrict input to digits only - let response = ui.add( - TextEdit::singleline(&mut self.decimals_input).desired_width(50.0) - ); + // Name should be capitalized + ui.horizontal(|ui| { + StyledCheckbox::new(&mut self.should_capitalize_input, "Name should be capitalized").show(ui); + crate::ui::helpers::info_icon_button(ui, "This is used only as helper information to client applications that will use token. This informs them on whether to capitalize the token name or not by default."); + }); + ui.end_row(); + + // Decimals + ui.horizontal(|ui| { + ui.label("Max Decimals:"); + // Restrict input to digits only + let response = ui.add( + TextEdit::singleline(&mut self.decimals_input).desired_width(50.0) + ); - // Optionally filter out non-digit input - if response.changed() { - self.decimals_input.retain(|c| c.is_ascii_digit()); - self.decimals_input.truncate(2); - } + // Optionally filter out non-digit input + if response.changed() { + self.decimals_input.retain(|c| c.is_ascii_digit()); + self.decimals_input.truncate(2); + } - let token_name = self.token_names_input - .first() - .as_ref() - .and_then(|(_, name, _, _)| if name.is_empty() { None} else { Some(name.as_str())}) - .unwrap_or(""); + let token_name = self.token_names_input + .first() + .as_ref() + .and_then(|(_, name, _, _)| if name.is_empty() { None} else { Some(name.as_str())}) + .unwrap_or(""); - let message = if self.decimals_input == "0" { - format!("Non Fractional Token (i.e. 0, 1, 2 or 10 {})", token_name) - } else { - format!("Fractional Token (i.e. 0.2 {})", token_name) - }; + let message = if self.decimals_input == "0" { + format!("Non Fractional Token (i.e. 0, 1, 2 or 10 {})", token_name) + } else { + format!("Fractional Token (i.e. 0.2 {})", token_name) + }; - ui.label(RichText::new(message).color(Color32::GRAY)); + ui.label(RichText::new(message).color(Color32::GRAY)); + crate::ui::helpers::info_icon_button(ui, "The decimal places of the token, for example Dash and Bitcoin use 8. The minimum indivisible amount is a Duff or a Satoshi respectively. If you put a value greater than 0 this means that it is indicated that the consensus is that 10^(number entered) is what represents 1 full unit of the token."); + }); + ui.end_row(); + + // Marketplace Trade Mode + ui.horizontal(|ui| { + ui.label("Marketplace Trade Mode:"); + ComboBox::from_id_salt("marketplace_trade_mode_selector") + .selected_text("Not Tradeable") + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.marketplace_trade_mode, + 0, + "Not Tradeable", + ); + // Future trade modes can be added here when SDK supports them + }); - crate::ui::helpers::info_icon_button(ui, "The decimal places of the token, for example Dash and Bitcoin use 8. The minimum indivisible amount is a Duff or a Satoshi respectively. If you put a value greater than 0 this means that it is indicated that the consensus is that 10^(number entered) is what represents 1 full unit of the token."); + crate::ui::helpers::info_icon_button(ui, + "Currently, all tokens are created as 'Not Tradeable'. \ + Future updates will add more trade mode options.\n\n\ + IMPORTANT: If you want to enable marketplace trading in the future, \ + make sure to set the 'Marketplace Trade Mode Change' rules in the Action Rules \ + section to something other than 'No One'. Otherwise, trading can never be enabled." + ); + }); + ui.end_row(); }); - ui.end_row(); + }); + } - // Marketplace Trade Mode - ui.horizontal(|ui| { - ui.label("Marketplace Trade Mode:"); - ComboBox::from_id_salt("marketplace_trade_mode_selector") - .selected_text("Not Tradeable") - .show_ui(ui, |ui| { - ui.selectable_value( - &mut self.marketplace_trade_mode, - 0, - "Not Tradeable", - ); - // Future trade modes can be added here when SDK supports them - }); + ui.add_space(5.0); - crate::ui::helpers::info_icon_button(ui, - "Currently, all tokens are created as 'Not Tradeable'. \ - Future updates will add more trade mode options.\n\n\ - IMPORTANT: If you want to enable marketplace trading in the future, \ - make sure to set the 'Marketplace Trade Mode Change' rules in the Action Rules \ - section to something other than 'No One'. Otherwise, trading can never be enabled." - ); - }); - ui.end_row(); - }); + ui.horizontal(|ui| { + // +/- button + let button_text = if self.token_creator_action_rules_expanded { "−" } else { "+" }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if button_response.clicked() { + self.token_creator_action_rules_expanded = !self.token_creator_action_rules_expanded; + } + ui.label("Action Rules"); }); - ui.add_space(5.0); + if self.token_creator_action_rules_expanded { + ui.add_space(3.0); - ClickableCollapsingHeader::new("Action Rules") - .id_salt("token_creator_action_rules") - .default_open(false) - .open(if self.should_reset_collapsing_states { Some(false) } else { None }) - .show(ui, |ui| { ui.horizontal(|ui| { + ui.add_space(40.0); // Indentation ui.label("Preset:"); ComboBox::from_id_salt("preset_selector") @@ -441,24 +475,48 @@ impl TokensScreen { }); }); - self.manual_minting_rules.render_mint_control_change_rules_ui(ui, &self.groups_ui, &mut self.new_tokens_destination_identity_should_default_to_contract_owner, &mut self.new_tokens_destination_other_identity_enabled, &mut self.minting_allow_choosing_destination, &mut self.new_tokens_destination_identity_rules, &mut self.new_tokens_destination_other_identity, &mut self.minting_allow_choosing_destination_rules); - self.manual_burning_rules.render_control_change_rules_ui(ui, &self.groups_ui,"Manual Burn", None); - self.freeze_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Freeze", Some(&mut self.allow_transfers_to_frozen_identities)); - self.unfreeze_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Unfreeze", None); - self.destroy_frozen_funds_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Destroy Frozen Funds", None); - self.emergency_action_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Emergency Action", None); - self.max_supply_change_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Max Supply Change", None); - self.conventions_change_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Conventions Change", None); - self.marketplace_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Marketplace Trade Mode Change", None); - self.change_direct_purchase_pricing_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Direct Purchase Pricing Change", None); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.add_space(20.0); // Indentation for action rules + ui.vertical(|ui| { + self.manual_minting_rules.render_mint_control_change_rules_ui(ui, &self.groups_ui, &mut self.new_tokens_destination_identity_should_default_to_contract_owner, &mut self.new_tokens_destination_other_identity_enabled, &mut self.minting_allow_choosing_destination, &mut self.new_tokens_destination_identity_rules, &mut self.new_tokens_destination_other_identity, &mut self.minting_allow_choosing_destination_rules, &mut self.token_creator_manual_mint_expanded, &mut self.token_creator_new_tokens_destination_expanded, &mut self.token_creator_minting_allow_choosing_expanded); + self.manual_burning_rules.render_control_change_rules_ui(ui, &self.groups_ui,"Manual Burn", None, &mut self.token_creator_manual_burn_expanded); + self.freeze_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Freeze", Some(&mut self.allow_transfers_to_frozen_identities), &mut self.token_creator_freeze_expanded); + self.unfreeze_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Unfreeze", None, &mut self.token_creator_unfreeze_expanded); + self.destroy_frozen_funds_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Destroy Frozen Funds", None, &mut self.token_creator_destroy_frozen_expanded); + self.emergency_action_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Emergency Action", None, &mut self.token_creator_emergency_action_expanded); + self.max_supply_change_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Max Supply Change", None, &mut self.token_creator_max_supply_change_expanded); + self.conventions_change_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Conventions Change", None, &mut self.token_creator_conventions_change_expanded); + self.marketplace_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Marketplace Trade Mode Change", None, &mut self.token_creator_marketplace_expanded); + self.change_direct_purchase_pricing_rules.render_control_change_rules_ui(ui, &self.groups_ui, "Direct Purchase Pricing Change", None, &mut self.token_creator_direct_purchase_pricing_expanded); + }); + }); // Main control group change is slightly different so do this one manually. ui.add_space(6.0); - ClickableCollapsingHeader::new("Main Control Group Change") - .id_salt("token_creator_main_control_group") - .default_open(false) - .open(if self.should_reset_collapsing_states { Some(false) } else { None }) - .show(ui, |ui| { + ui.horizontal(|ui| { + ui.add_space(20.0); // Indentation for main control group change + ui.vertical(|ui| { + ui.horizontal(|ui| { + // +/- button + let button_text = if self.token_creator_main_control_expanded { "−" } else { "+" }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if button_response.clicked() { + self.token_creator_main_control_expanded = !self.token_creator_main_control_expanded; + } + ui.label("Main Control Group Change"); + }); + + if self.token_creator_main_control_expanded { ui.add_space(3.0); // A) authorized_to_make_change @@ -516,8 +574,10 @@ impl TokensScreen { _ => {} } }); + } + }); }); - }); + } self.render_distributions(context, ui); self.render_groups(ui); @@ -595,10 +655,30 @@ impl TokensScreen { } } }); - }); - // Reset the flag after processing all collapsing headers + // Reset the expanded states after processing if self.should_reset_collapsing_states { + self.token_creator_advanced_expanded = false; + self.token_creator_action_rules_expanded = false; + self.token_creator_main_control_expanded = false; + self.token_creator_distribution_expanded = false; + self.token_creator_groups_expanded = false; + self.token_creator_document_schemas_expanded = false; + // Individual action rules + self.token_creator_manual_mint_expanded = false; + self.token_creator_manual_burn_expanded = false; + self.token_creator_freeze_expanded = false; + self.token_creator_unfreeze_expanded = false; + self.token_creator_destroy_frozen_expanded = false; + self.token_creator_emergency_action_expanded = false; + self.token_creator_max_supply_change_expanded = false; + self.token_creator_conventions_change_expanded = false; + self.token_creator_marketplace_expanded = false; + self.token_creator_direct_purchase_pricing_expanded = false; + // Nested rules + self.token_creator_new_tokens_destination_expanded = false; + self.token_creator_minting_allow_choosing_expanded = false; + self.token_creator_perpetual_distribution_rules_expanded = false; self.should_reset_collapsing_states = false; } @@ -632,6 +712,8 @@ impl TokensScreen { ui.add_space(10.0); } + }); // Close the ScrollArea from line 40 + action } @@ -771,19 +853,18 @@ impl TokensScreen { .parse::() .map_err(|_| "Invalid decimal places amount".to_string())?; let base_supply = self - .base_supply_input - .parse::() - .map_err(|_| "Invalid base supply amount".to_string())?; - let max_supply = if self.max_supply_input.is_empty() { - None - } else { - // If parse fails, error out - Some( - self.max_supply_input - .parse::() - .map_err(|_| "Invalid Max Supply".to_string())?, - ) - }; + .base_supply_amount + .as_ref() + .map(|amount| amount.value()) + .ok_or_else(|| "Please enter a valid base supply amount".to_string())?; + let max_supply = self + .max_supply_amount + .as_ref() + .map(|amount| { + let value = amount.value(); + if value > 0 { Some(value) } else { None } + }) + .unwrap_or(None); let start_paused = self.start_as_paused_input; let allow_transfers_to_frozen_identities = self.allow_transfers_to_frozen_identities; @@ -972,59 +1053,66 @@ impl TokensScreen { /// Shows a popup "Are you sure?" for creating the token contract fn render_token_creator_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; - let mut is_open = true; - - egui::Window::new("Confirm Token Contract Registration") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label( - "Are you sure you want to register a new token contract with these settings?\n", - ); - let max_supply_display = if self.max_supply_input.is_empty() { - "None".to_string() - } else { - self.max_supply_input.clone() - }; - ui.label(format!( - "Name: {}\nBase Supply: {}\nMax Supply: {}", - self.token_names_input[0].0, self.base_supply_input, max_supply_display, - )); - ui.add_space(10.0); - - ui.label(format!( - "Estimated cost to register this token is {} Dash", - self.estimate_registration_cost() as f64 / 100_000_000_000.0 - )); + // Prepare the confirmation message + let mut confirmation_message = + "Are you sure you want to register a new token contract with these settings?\n\n" + .to_string(); + let base_supply_display = self + .base_supply_amount + .as_ref() + .map(|amount| amount.to_string_opts(true, false)) + .unwrap_or_else(|| "0".to_string()); + let max_supply_display = self + .max_supply_amount + .as_ref() + .filter(|amount| amount.value() > 0) + .map(|amount| amount.to_string_opts(true, false)) + .unwrap_or_else(|| "None".to_string()); + + confirmation_message.push_str(&format!( + "Name: {}\nBase Supply: {}\nMax Supply: {}\n\n", + self.token_names_input[0].0, base_supply_display, max_supply_display, + )); + + confirmation_message.push_str(&format!( + "Estimated cost to register this token is {} Dash", + self.estimate_registration_cost() as f64 / 100_000_000_000.0 + )); + + // Check if marketplace is locked to NotTradeable forever + let mut is_danger_mode = false; + if let Some(args) = &self.cached_build_args { + let is_not_tradeable = args.marketplace_trade_mode == 0; + let marketplace_rules_locked = matches!( + args.marketplace_rules, + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::NoOne, + admin_action_takers: AuthorizedActionTakers::NoOne, + .. + }) + ); - ui.add_space(10.0); - - // Check if marketplace is locked to NotTradeable forever - if let Some(args) = &self.cached_build_args { - let is_not_tradeable = args.marketplace_trade_mode == 0; - let marketplace_rules_locked = matches!( - args.marketplace_rules, - ChangeControlRules::V0(ChangeControlRulesV0 { - authorized_to_make_change: AuthorizedActionTakers::NoOne, - admin_action_takers: AuthorizedActionTakers::NoOne, - .. - }) - ); + if is_not_tradeable && marketplace_rules_locked { + confirmation_message.push_str("\n\nWARNING: This token will be permanently set to NotTradeable and can NEVER be made tradeable in the future!"); + is_danger_mode = true; + } + } - if is_not_tradeable && marketplace_rules_locked { - ui.colored_label( - Color32::DARK_RED, - "WARNING: This token will be permanently set to NotTradeable and can NEVER be made tradeable in the future!" - ); - ui.add_space(10.0); - } - } + // Always create a fresh confirmation dialog to ensure current state is reflected + let confirmation_dialog = self.token_creator_confirmation_dialog.insert( + ConfirmationDialog::new("Confirm Token Contract Registration", confirmation_message) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + .danger_mode(is_danger_mode), + ); - ui.add_space(10.0); + // Show the dialog and handle the response + let response = confirmation_dialog.show(ui).inner; - // Confirm - if ui.button("Confirm").clicked() { + if let Some(status) = response.dialog_response { + match status { + ConfirmationStatus::Confirmed => { let args = match &self.cached_build_args { Some(args) => args.clone(), None => { @@ -1034,8 +1122,8 @@ impl TokensScreen { Err(err) => { self.token_creator_error_message = Some(err); self.show_token_creator_confirmation_popup = false; - action = AppAction::None; - return; + self.token_creator_confirmation_dialog = None; + return AppAction::None; } } } @@ -1083,17 +1171,15 @@ impl TokensScreen { self.show_token_creator_confirmation_popup = false; let now = Utc::now().timestamp() as u64; self.token_creator_status = TokenCreatorStatus::WaitingForResult(now); + self.show_token_creator_confirmation_popup = false; + self.token_creator_confirmation_dialog = None; } - - // Cancel - if ui.button("Cancel").clicked() { + ConfirmationStatus::Canceled => { self.show_token_creator_confirmation_popup = false; + self.token_creator_confirmation_dialog = None; action = AppAction::None; } - }); - - if !is_open { - self.show_token_creator_confirmation_popup = false; + } } action @@ -1103,15 +1189,35 @@ impl TokensScreen { fn render_document_schemas(&mut self, ui: &mut Ui) { ui.add_space(5.0); - ClickableCollapsingHeader::new("Document Schemas") - .id_salt("token_creator_document_schemas") - .default_open(false) - .open(if self.should_reset_collapsing_states { Some(false) } else { None }) - .show(ui, |ui| { - ui.add_space(3.0); + ui.horizontal(|ui| { + // +/- button + let button_text = if self.token_creator_document_schemas_expanded { + "−" + } else { + "+" + }; + let button_response = ui.add( + egui::Button::new( + RichText::new(button_text) + .size(20.0) + .color(DashColors::DASH_BLUE), + ) + .fill(Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if button_response.clicked() { + self.token_creator_document_schemas_expanded = + !self.token_creator_document_schemas_expanded; + } + ui.label("Document Schemas"); + }); + + if self.token_creator_document_schemas_expanded { + ui.add_space(3.0); + ui.indent("document_schemas_section", |ui| { // Add link to dashpay.io - ui.horizontal(|ui| { + ui.horizontal(|ui| { ui.label("Paste JSON document schemas to include in the contract. Easily create document schemas here:"); ui.add(egui::Hyperlink::from_label_and_url( RichText::new("dashpay.io") @@ -1121,38 +1227,39 @@ impl TokensScreen { )); }); - ui.add_space(5.0); + ui.add_space(5.0); - let dark_mode = ui.ctx().style().visuals.dark_mode; - let schemas_response = ui.add_sized( - [ui.available_width(), 120.0], - TextEdit::multiline(&mut self.document_schemas_input) - .text_color(crate::ui::theme::DashColors::text_primary(dark_mode)) - .background_color(crate::ui::theme::DashColors::input_background(dark_mode)), - ); + let dark_mode = ui.ctx().style().visuals.dark_mode; + let schemas_response = ui.add_sized( + [ui.available_width(), 120.0], + TextEdit::multiline(&mut self.document_schemas_input) + .text_color(crate::ui::theme::DashColors::text_primary(dark_mode)) + .background_color(crate::ui::theme::DashColors::input_background(dark_mode)), + ); - if schemas_response.changed() { - self.parse_document_schemas(); - } + if schemas_response.changed() { + self.parse_document_schemas(); + } - ui.add_space(5.0); + ui.add_space(5.0); - // Show validation result - if let Some(ref error) = self.document_schemas_error { + // Show validation result + if let Some(ref error) = self.document_schemas_error { + ui.colored_label( + Color32::DARK_RED, + format!("Schema validation error: {}", error), + ); + } else if self.parsed_document_schemas.is_some() { + let schema_count = self.parsed_document_schemas.as_ref().unwrap().len(); + if schema_count > 0 { ui.colored_label( - Color32::DARK_RED, - format!("Schema validation error: {}", error), + Color32::DARK_GREEN, + format!("✓ {} valid document schema(s) parsed", schema_count), ); - } else if self.parsed_document_schemas.is_some() { - let schema_count = self.parsed_document_schemas.as_ref().unwrap().len(); - if schema_count > 0 { - ui.colored_label( - Color32::DARK_GREEN, - format!("✓ {} valid document schema(s) parsed", schema_count), - ); - } } + } }); + } } /// Parse and validate the document schemas JSON input diff --git a/src/ui/tokens/transfer_tokens_screen.rs b/src/ui/tokens/transfer_tokens_screen.rs index 2c12cb9da..41effdd87 100644 --- a/src/ui/tokens/transfer_tokens_screen.rs +++ b/src/ui/tokens/transfer_tokens_screen.rs @@ -1,9 +1,14 @@ -use crate::app::{AppAction, BackendTasksExecutionMode}; +use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; +use crate::model::amount::Amount; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::component_trait::{Component, ComponentResponse}; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -16,7 +21,6 @@ use crate::ui::theme::DashColors; use crate::ui::{MessageType, Screen, ScreenLike}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; -use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::dpp::prelude::TimestampMillis; use dash_sdk::platform::{Identifier, IdentityPublicKey}; use eframe::egui::{self, Context, Ui}; @@ -28,83 +32,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; use crate::ui::identities::get_selected_wallet; use super::tokens_screen::IdentityTokenBalance; -use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use dash_sdk::dpp::data_contract::associated_token::token_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters; - -fn format_token_amount(amount: u64, decimals: u8) -> String { - if decimals == 0 { - return amount.to_string(); - } - - let divisor = 10u64.pow(decimals as u32); - let whole = amount / divisor; - let fraction = amount % divisor; - - if fraction == 0 { - whole.to_string() - } else { - // Format with the appropriate number of decimal places, removing trailing zeros - let fraction_str = format!("{:0width$}", fraction, width = decimals as usize); - let trimmed = fraction_str.trim_end_matches('0'); - format!("{}.{}", whole, trimmed) - } -} - -fn parse_token_amount(input: &str, decimals: u8) -> Result { - if decimals == 0 { - return input - .parse::() - .map_err(|_| "Invalid amount: must be a whole number".to_string()); - } - - let parts: Vec<&str> = input.split('.').collect(); - match parts.len() { - 1 => { - // No decimal point, parse as whole number - let whole = parts[0] - .parse::() - .map_err(|_| "Invalid amount: must be a number".to_string())?; - let multiplier = 10u64.pow(decimals as u32); - whole - .checked_mul(multiplier) - .ok_or_else(|| "Amount too large".to_string()) - } - 2 => { - // Has decimal point - let whole = if parts[0].is_empty() { - 0 - } else { - parts[0] - .parse::() - .map_err(|_| "Invalid amount: whole part must be a number".to_string())? - }; - - let fraction_str = parts[1]; - if fraction_str.len() > decimals as usize { - return Err(format!( - "Too many decimal places. Maximum allowed: {}", - decimals - )); - } - - // Pad with zeros if needed - let padded_fraction = format!("{:0() - .map_err(|_| "Invalid amount: decimal part must be a number".to_string())?; - - let multiplier = 10u64.pow(decimals as u32); - let whole_part = whole - .checked_mul(multiplier) - .ok_or_else(|| "Amount too large".to_string())?; - - whole_part - .checked_add(fraction) - .ok_or_else(|| "Amount too large".to_string()) - } - _ => Err("Invalid amount: too many decimal points".to_string()), - } -} #[derive(PartialEq)] pub enum TransferTokensStatus { @@ -117,16 +44,16 @@ pub enum TransferTokensStatus { pub struct TransferTokensScreen { pub identity: QualifiedIdentity, pub identity_token_balance: IdentityTokenBalance, - friend_identities: Vec<(String, Identifier)>, - selected_friend_index: Option, + known_identities: Vec, selected_key: Option, pub public_note: Option, pub receiver_identity_id: String, - pub amount: String, + pub amount: Option, + pub amount_input: Option, transfer_tokens_status: TransferTokensStatus, - max_amount: u64, + max_amount: Amount, pub app_context: Arc, - confirmation_popup: bool, + confirmation_dialog: Option, selected_wallet: Option>>, wallet_password: String, show_password: bool, @@ -137,28 +64,16 @@ impl TransferTokensScreen { identity_token_balance: IdentityTokenBalance, app_context: &Arc, ) -> Self { - let all_identities = app_context + let known_identities = app_context .load_local_qualified_identities() .expect("Identities not loaded"); - let friend_identities: Vec<(String, Identifier)> = all_identities - .iter() - .filter(|id| id.identity.id() != identity_token_balance.identity_id) - .map(|id| { - let alias = id - .alias - .clone() - .unwrap_or_else(|| id.identity.id().to_string(Encoding::Base58)); - (alias, id.identity.id()) - }) - .collect(); - - let identity = all_identities + let identity = known_identities .iter() .find(|identity| identity.identity.id() == identity_token_balance.identity_id) .expect("Identity not found") .clone(); - let max_amount = identity_token_balance.balance; + let max_amount = Amount::from(&identity_token_balance); let identity_clone = identity.identity.clone(); let selected_key = identity_clone.get_first_public_key_matching( Purpose::AUTHENTICATION, @@ -170,25 +85,21 @@ impl TransferTokensScreen { let selected_wallet = get_selected_wallet(&identity, None, selected_key, &mut error_message); - let (selected_friend_index, receiver_identity_id) = - if let Some((_first, identifier)) = friend_identities.first() { - (Some(0), identifier.to_string(Encoding::Base58)) - } else { - (None, String::new()) - }; + let amount = Some(Amount::from(&identity_token_balance).with_value(0)); + Self { identity, identity_token_balance, - friend_identities, - selected_friend_index, + known_identities, selected_key: selected_key.cloned(), public_note: None, - receiver_identity_id, - amount: String::new(), + receiver_identity_id: String::new(), + amount, + amount_input: None, transfer_tokens_status: TransferTokensStatus::NotStarted, max_amount, app_context: app_context.clone(), - confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -196,149 +107,131 @@ impl TransferTokensScreen { } fn render_amount_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Amount:"); - - ui.text_edit_singleline(&mut self.amount); - - if ui.button("Max").clicked() { - let decimals = self - .identity_token_balance - .token_config - .conventions() - .decimals(); - self.amount = format_token_amount(self.max_amount, decimals); + ui.label(format!("Available balance: {}", self.max_amount)); + ui.add_space(5.0); + + // Lazy initialization with proper decimal places + let amount_input = match self.amount_input.as_mut() { + Some(input) => input, + _ => { + self.amount_input = Some( + AmountInput::new( + self.amount + .as_ref() + .unwrap_or(&Amount::from(&self.identity_token_balance)), + ) + .with_label("Amount:") + .with_max_button(true), + ); + + self.amount_input + .as_mut() + .expect("AmountInput should be initialized above") } - }); + }; + + // Check if input should be disabled when operation is in progress + let enabled = match self.transfer_tokens_status { + TransferTokensStatus::WaitingForResult(_) | TransferTokensStatus::Complete => false, + TransferTokensStatus::NotStarted | TransferTokensStatus::ErrorMessage(_) => { + amount_input.set_max_amount(Some(self.max_amount.value())); + true + } + }; + + let response = ui.add_enabled_ui(enabled, |ui| amount_input.show(ui)).inner; + + response.inner.update(&mut self.amount); + // errors are handled inside AmountInput } fn render_to_identity_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - // Dropdown - egui::ComboBox::from_id_salt("friend_selector") - .selected_text( - self.selected_friend_index - .and_then(|i| self.friend_identities.get(i).map(|(name, _)| name.clone())) - .unwrap_or_else(|| "Other".to_string()), - ) - .show_ui(ui, |ui| { - for (i, (alias, _)) in self.friend_identities.iter().enumerate() { - if ui - .selectable_value(&mut self.selected_friend_index, Some(i), alias) - .clicked() - { - self.receiver_identity_id = - self.friend_identities[i].1.to_string(Encoding::Base58); - } - } + let _response = ui.add( + IdentitySelector::new( + "transfer_recipient_selector", + &mut self.receiver_identity_id, + &self.known_identities, + ) + .width(300.0) + .label("Recipient:") + .exclude(&[self.identity.identity.id()]), + ); + } - if ui - .selectable_value(&mut self.selected_friend_index, None, "Other") - .clicked() - { - // Clear the text box to avoid confusion - self.receiver_identity_id.clear(); - } - }); + fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { + let msg = format!( + "Are you sure you want to transfer {} tokens to {}?", + self.amount.clone().unwrap_or(Amount::new(0, 0)), + self.receiver_identity_id + ); - // Text box - let prev_text = self.receiver_identity_id.clone(); - ui.text_edit_singleline(&mut self.receiver_identity_id); - if self.receiver_identity_id != prev_text { - self.selected_friend_index = None; - } + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Transfer", msg) + .confirm_text(Some("Transfer")) + .cancel_text(Some("Cancel")) }); - } - fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut app_action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Transfer") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - let identifier = if self.receiver_identity_id.is_empty() { - self.transfer_tokens_status = - TransferTokensStatus::ErrorMessage("Invalid identifier".to_string()); - self.confirmation_popup = false; - return; - } else { - match Identifier::from_string_try_encodings( - &self.receiver_identity_id, - &[Encoding::Base58, Encoding::Hex], - ) { - Ok(identifier) => identifier, - Err(_) => { - self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( - "Invalid identifier".to_string(), - ); - self.confirmation_popup = false; - return; - } - } - }; - - if self.selected_key.is_none() { - self.transfer_tokens_status = - TransferTokensStatus::ErrorMessage("No selected key".to_string()); - self.confirmation_popup = false; - return; - }; - - ui.label(format!( - "Are you sure you want to transfer {} {} to {}?", - self.amount, self.identity_token_balance.token_alias, self.receiver_identity_id - )); - - if ui.button("Confirm").clicked() { - self.confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.transfer_tokens_status = TransferTokensStatus::WaitingForResult(now); - let data_contract = Arc::new( - self.app_context - .get_unqualified_contract_by_id( - &self.identity_token_balance.data_contract_id, - ) - .expect("Contracts not loaded") - .expect("Data contract not found"), - ); - app_action |= AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::TransferTokens { - sending_identity: self.identity.clone(), - recipient_id: identifier, - amount: { - let decimals = self - .identity_token_balance - .token_config - .conventions() - .decimals(); - parse_token_amount(&self.amount, decimals) - .expect("Amount should be valid at this point") - }, - data_contract, - token_position: self.identity_token_balance.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: self.public_note.clone(), - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } - if ui.button("Cancel").clicked() { - self.confirmation_popup = false; - } - }); - if !is_open { - self.confirmation_popup = false; + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - app_action } + fn confirmation_ok(&mut self) -> AppAction { + if self.amount.is_none() || self.amount == Some(Amount::new(0, 0)) { + self.transfer_tokens_status = + TransferTokensStatus::ErrorMessage("Invalid amount".into()); + return AppAction::None; + } + + let parsed_receiver_id = Identifier::from_string_try_encodings( + &self.receiver_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + + if parsed_receiver_id.is_err() { + self.transfer_tokens_status = + TransferTokensStatus::ErrorMessage("Invalid receiver".into()); + return AppAction::None; + } + + let receiver_id = parsed_receiver_id.unwrap(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.transfer_tokens_status = TransferTokensStatus::WaitingForResult(now); + + let data_contract = Arc::new( + self.app_context + .get_unqualified_contract_by_id(&self.identity_token_balance.data_contract_id) + .expect("Failed to get data contract") + .expect("Data contract not found"), + ); + + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::TransferTokens { + sending_identity: self.identity.clone(), + recipient_id: receiver_id, + amount: self.amount.clone().unwrap_or(Amount::new(0, 0)).value(), + data_contract, + token_position: self.identity_token_balance.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: self.public_note.clone(), + }, + ))) + } pub fn show_success(&self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; @@ -396,8 +289,8 @@ impl ScreenLike for TransferTokensScreen { self.max_amount = token_balances .values() .find(|balance| balance.identity_id == self.identity.identity.id()) - .map(|balance| balance.balance) - .unwrap_or(0); + .map(Amount::from) + .unwrap_or_default(); } /// Renders the UI components for the withdrawal screen @@ -515,19 +408,6 @@ impl ScreenLike for TransferTokensScreen { ui.heading("2. Input the amount to transfer"); ui.add_space(5.0); - // Show available balance - let decimals = self - .identity_token_balance - .token_config - .conventions() - .decimals(); - let formatted_balance = format_token_amount(self.max_amount, decimals); - ui.label(format!( - "Available balance: {} {}", - formatted_balance, self.identity_token_balance.token_alias - )); - ui.add_space(5.0); - self.render_amount_input(ui); ui.add_space(10.0); @@ -563,6 +443,10 @@ impl ScreenLike for TransferTokensScreen { ui.add_space(10.0); // Transfer button + + let ready = self.amount.is_some() + && !self.receiver_identity_id.is_empty() + && self.selected_key.is_some(); let mut new_style = (**ui.style()).clone(); new_style.spacing.button_padding = egui::vec2(10.0, 5.0); ui.set_style(new_style); @@ -570,33 +454,35 @@ impl ScreenLike for TransferTokensScreen { .fill(Color32::from_rgb(0, 128, 255)) .frame(true) .corner_radius(3.0); - if ui.add(button).clicked() { - let decimals = self - .identity_token_balance - .token_config - .conventions() - .decimals(); - match parse_token_amount(&self.amount, decimals) { - Ok(parsed_amount) => { - if parsed_amount > self.max_amount { - self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( - "Amount exceeds available balance".to_string(), - ); - } else if parsed_amount == 0 { - self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( - "Amount must be greater than zero".to_string(), - ); - } else { - self.confirmation_popup = true; - } - } - Err(e) => { - self.transfer_tokens_status = TransferTokensStatus::ErrorMessage(e); - } + if ui + .add_enabled(ready, button) + .on_disabled_hover_text("Please ensure all fields are filled correctly") + .clicked() + { + // Use the amount value directly since it's already parsed + if self.amount.as_ref().is_some_and(|v| v > &self.max_amount) { + self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( + "Amount exceeds available balance".to_string(), + ); + } else if self.amount.as_ref().is_none_or(|a| a.value() == 0) { + self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( + "Amount must be greater than zero".to_string(), + ); + } else { + let msg = format!( + "Are you sure you want to transfer {} tokens to {}?", + self.amount.clone().unwrap_or(Amount::new(0, 0)), + self.receiver_identity_id + ); + self.confirmation_dialog = Some( + ConfirmationDialog::new("Confirm Transfer", msg) + .confirm_text(Some("Transfer")) + .cancel_text(Some("Cancel")), + ); } } - if self.confirmation_popup { + if self.confirmation_dialog.is_some() { return self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/unfreeze_tokens_screen.rs b/src/ui/tokens/unfreeze_tokens_screen.rs index 3c7bd218e..2f891b837 100644 --- a/src/ui/tokens/unfreeze_tokens_screen.rs +++ b/src/ui/tokens/unfreeze_tokens_screen.rs @@ -5,6 +5,9 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -43,7 +46,7 @@ pub enum UnfreezeTokensStatus { Complete, } -/// A screen that allows unfreezing a previously frozen identity’s tokens for a specific contract +/// A screen that allows unfreezing a previously frozen identity's tokens for a specific contract pub struct UnfreezeTokensScreen { pub identity: QualifiedIdentity, pub identity_token_info: IdentityTokenInfo, @@ -53,6 +56,10 @@ pub struct UnfreezeTokensScreen { group: Option<(GroupContractPosition, Group)>, is_unilateral_group_member: bool, pub group_action_id: Option, + /// A list of identities that are frozen and can be unfrozen. + /// + /// TODO: Right now it is just a list of all identities, but it should be filtered to only show frozen ones. + frozen_identities: Vec, /// The identity we want to freeze pub unfreeze_identity_id: String, @@ -63,8 +70,8 @@ pub struct UnfreezeTokensScreen { // Basic references pub app_context: Arc, - // Confirmation popup - show_confirmation_popup: bool, + // Confirmation dialog + confirmation_dialog: Option, // If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -74,6 +81,11 @@ pub struct UnfreezeTokensScreen { impl UnfreezeTokensScreen { pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + // TODO: filter to include only frozen identities + let frozen_identities = app_context + .load_local_qualified_identities() + .expect("Identities not loaded"); + let possible_key = identity_token_info .identity .identity @@ -153,17 +165,17 @@ impl UnfreezeTokensScreen { }; let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } + if group.is_some() + && let Some((_, group)) = group.clone() + { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power + && your_power >= &group.required_power() + { + is_unilateral_group_member = true; } }; @@ -187,107 +199,109 @@ impl UnfreezeTokensScreen { status: UnfreezeTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, + frozen_identities, } } fn render_unfreeze_identity_input(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.label("Identity to Unfreeze:"); - ui.text_edit_singleline(&mut self.unfreeze_identity_id); - }); + let _response = ui.add( + IdentitySelector::new( + "unfreeze_identity_selector", + &mut self.unfreeze_identity_id, + &self.frozen_identities, + ) + .width(300.0) + .label("Identity ID to unfreeze:"), + ); } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Unfreeze") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Validate user input - let parsed = Identifier::from_string_try_encodings( - &self.unfreeze_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - if parsed.is_err() { - self.error_message = Some("Please enter a valid identity ID.".into()); - self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); - self.show_confirmation_popup = false; - return; - } - let unfreeze_id = parsed.unwrap(); - - ui.label(format!( - "Are you sure you want to unfreeze identity {}?", - self.unfreeze_identity_id - )); - - ui.add_space(10.0); + let msg = format!( + "Are you sure you want to unfreeze identity {}?", + self.unfreeze_identity_id + ); - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = UnfreezeTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch to backend - action |= AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::UnfreezeTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - unfreeze_identity: unfreeze_id, - group_info, - }, - ))); - } + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Unfreeze", msg) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + }); - // Cancel - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, + } + } - if !is_open { - self.show_confirmation_popup = false; + fn confirmation_ok(&mut self) -> AppAction { + // Validate user input + let parsed = Identifier::from_string_try_encodings( + &self.unfreeze_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + if parsed.is_err() { + self.error_message = Some("Please enter a valid identity ID.".into()); + self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); + return AppAction::None; } - action + let unfreeze_id = parsed.unwrap(); + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = UnfreezeTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Dispatch to backend + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::UnfreezeTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + unfreeze_identity: unfreeze_id, + group_info, + }, + ))) } fn show_success_screen(&self, ui: &mut Ui) -> AppAction { @@ -354,13 +368,12 @@ impl ScreenLike for UnfreezeTokensScreen { } fn refresh(&mut self) { - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities + if let Ok(all_identities) = self.app_context.load_local_user_identities() + && let Some(updated_identity) = all_identities .into_iter() .find(|id| id.identity.id() == self.identity.identity.id()) - { - self.identity = updated_identity; - } + { + self.identity = updated_identity; } } @@ -548,12 +561,21 @@ impl ScreenLike for UnfreezeTokensScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + // Initialize confirmation dialog when button is clicked + let msg = format!( + "Are you sure you want to unfreeze identity {}?", + self.unfreeze_identity_id + ); + self.confirmation_dialog = Some( + ConfirmationDialog::new("Confirm Unfreeze", msg) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")), + ); } } - // If user pressed "Unfreeze," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/update_token_config.rs b/src/ui/tokens/update_token_config.rs index 9cd7e6a04..49460b84d 100644 --- a/src/ui/tokens/update_token_config.rs +++ b/src/ui/tokens/update_token_config.rs @@ -207,10 +207,10 @@ impl UpdateTokenConfigScreen { .members() .get(&self.identity_token_info.identity.identity.id()); - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - self.is_unilateral_group_member = true; - } + if let Some(your_power) = your_power + && your_power >= &group.required_power() + { + self.is_unilateral_group_member = true; } } } @@ -614,13 +614,12 @@ impl UpdateTokenConfigScreen { ui.label(&self.update_text); ui.horizontal(|ui| { - if let Some(opt_json) = opt_json { - if ui.button("View Current").clicked() { + if let Some(opt_json) = opt_json + && ui.button("View Current").clicked() { self.update_text = serde_json::to_string_pretty(opt_json).unwrap_or_default(); // Update displayed text } - } if !self.text_input_error.is_empty() { ui.colored_label(Color32::RED, &self.text_input_error); @@ -987,12 +986,11 @@ impl ScreenLike for UpdateTokenConfigScreen { // Central panel island_central_panel(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { - if let Some(msg) = &self.backend_message { - if msg.1 == MessageType::Success { + if let Some(msg) = &self.backend_message + && msg.1 == MessageType::Success { action |= self.show_success_screen(ui); return; } - } ui.heading("Update Token Configuration"); ui.add_space(10.0); diff --git a/src/ui/tools/grovestark_screen.rs b/src/ui/tools/grovestark_screen.rs new file mode 100644 index 000000000..e6f95532d --- /dev/null +++ b/src/ui/tools/grovestark_screen.rs @@ -0,0 +1,1183 @@ +use crate::app::AppAction; +use crate::backend_task::BackendTask; +use crate::backend_task::grovestark::GroveSTARKTask; +use crate::context::AppContext; +use crate::model::qualified_identity::{PrivateKeyTarget, QualifiedIdentity}; +use crate::ui::RootScreenType; +use crate::ui::ScreenLike; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::styled::island_central_panel; +use crate::ui::components::tools_subscreen_chooser_panel::add_tools_subscreen_chooser_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::theme::{DashColors, Shape, Spacing, Typography}; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dash_sdk::dpp::identity::{ + Identity, IdentityPublicKey, KeyType, Purpose, accessors::IdentityGettersV0, +}; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use egui::{Button, ComboBox, Context, Frame, Grid, Margin, RichText, ScrollArea, TextEdit, Ui}; +use std::sync::Arc; +use std::time::Duration; + +#[derive(Clone, PartialEq)] +pub enum ProofMode { + Generate, + Verify, +} + +#[derive(Clone)] +pub struct VerificationResult { + pub is_valid: bool, + pub verified_at: u64, + pub contract_id: String, + pub security_level: u32, + pub error_message: Option, + pub technical_details: String, +} + +#[derive(Clone)] +pub struct ProofData { + pub full_proof: crate::model::grovestark_prover::ProofDataOutput, + pub hash: String, + pub size: usize, + pub generation_time: Duration, +} + +pub struct GroveSTARKScreen { + pub(crate) app_context: Arc, + mode: ProofMode, + + // Generation fields + selected_identity: Option, + selected_key: Option, + selected_contract: Option, + selected_document_type: Option, + available_document_types: Vec, // Document types for selected contract + selected_document: Option, + available_identities: Vec, + qualified_identities: Vec, // Store full qualified identities for key access + available_contracts: Vec<(String, String)>, // (id, name) + // Documents will be entered directly via text input + is_generating: bool, + generated_proof: Option, + proof_size: Option, + generation_time: Option, + security_level: u32, + + // Verification fields + proof_text: String, + is_verifying: bool, + verification_result: Option, + + // Error handling + gen_error_message: Option, + verify_error_message: Option, +} + +impl GroveSTARKScreen { + pub fn new(app_context: &Arc) -> Self { + // Load initial qualified identities + let qualified_identities = app_context + .load_local_qualified_identities() + .unwrap_or_default(); + + let available_identities = qualified_identities + .iter() + .map(|qualified_identity| qualified_identity.identity.clone()) + .collect(); + + tracing::info!( + "ZK Proofs screen loaded {} identities", + qualified_identities.len() + ); + + // Load initial contracts (exclude system contracts) + let excluded_aliases = ["dpns", "keyword_search", "token_history", "withdrawals"]; + let all_contracts = app_context.get_contracts(None, None).unwrap_or_default(); + + tracing::info!( + "ZK Proofs screen found {} total contracts", + all_contracts.len() + ); + + let available_contracts: Vec<(String, String)> = all_contracts + .into_iter() + .filter(|c| match &c.alias { + Some(alias) => { + let is_system = excluded_aliases.contains(&alias.as_str()); + if is_system { + tracing::debug!("Excluding system contract: {}", alias); + } + !is_system + } + None => true, + }) + .map(|qualified_contract| { + let id = qualified_contract + .contract + .id() + .to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58); + let name = qualified_contract + .alias + .unwrap_or_else(|| format!("Contract {}", &id[..8])); + tracing::debug!("Including contract: {} ({})", name, id); + (id, name) + }) + .collect(); + + tracing::info!( + "ZK Proofs screen loaded {} user contracts after filtering", + available_contracts.len() + ); + + Self { + app_context: app_context.clone(), + mode: ProofMode::Generate, + selected_identity: None, + selected_key: None, + selected_contract: None, + selected_document_type: None, + available_document_types: Vec::new(), + selected_document: None, + available_identities, + qualified_identities, + available_contracts, + is_generating: false, + generated_proof: None, + proof_size: None, + generation_time: None, + security_level: 128, + proof_text: String::new(), + is_verifying: false, + verification_result: None, + gen_error_message: None, + verify_error_message: None, + } + } + + fn refresh_identities(&mut self, app_context: &AppContext) { + let all_qualified_identities = app_context + .load_local_qualified_identities() + .unwrap_or_default(); + + // Filter identities to only show those with EdDSA keys + self.qualified_identities = all_qualified_identities + .into_iter() + .filter(|qi| self.has_eddsa_keys(&qi.identity)) + .collect(); + + self.available_identities = self + .qualified_identities + .iter() + .map(|qualified_identity| qualified_identity.identity.clone()) + .collect(); + } + + fn get_qualified_identity(&self, identity_id_str: &str) -> Option<&QualifiedIdentity> { + self.qualified_identities + .iter() + .find(|qi| qi.identity.id().to_string(Encoding::Base58) == identity_id_str) + } + + /// Check if an identity has any EdDSA keys suitable for ZK proofs + fn has_eddsa_keys(&self, identity: &Identity) -> bool { + identity.public_keys().iter().any(|(_, key)| { + matches!(key.key_type(), KeyType::EDDSA_25519_HASH160) + && (key.purpose() == Purpose::AUTHENTICATION || key.purpose() == Purpose::TRANSFER) + }) + } + + fn get_available_keys(&self, identity_id_str: &str) -> Vec<&IdentityPublicKey> { + if let Some(qualified_identity) = self.get_qualified_identity(identity_id_str) { + qualified_identity + .private_keys + .identity_public_keys() + .into_iter() + .filter(|(target, _)| **target == PrivateKeyTarget::PrivateKeyOnMainIdentity) + .map(|(_, key_ref)| &key_ref.identity_public_key) + .filter(|key| { + // Only show EdDSA keys suitable for signing + matches!(key.key_type(), KeyType::EDDSA_25519_HASH160) + && (key.purpose() == Purpose::AUTHENTICATION + || key.purpose() == Purpose::TRANSFER) + }) + .collect() + } else { + Vec::new() + } + } + + fn refresh_contracts(&mut self, app_context: &AppContext) { + let excluded_aliases = ["dpns", "keyword_search", "token_history", "withdrawals"]; + let all_contracts = app_context.get_contracts(None, None).unwrap_or_default(); + + self.available_contracts = all_contracts + .into_iter() + .filter(|c| match &c.alias { + Some(alias) => !excluded_aliases.contains(&alias.as_str()), + None => true, + }) + .map(|qualified_contract| { + let id = qualified_contract + .contract + .id() + .to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58); + let name = qualified_contract + .alias + .unwrap_or_else(|| format!("Contract {}", &id[..8])); + (id, name) + }) + .collect(); + + tracing::info!( + "Refreshed contracts: found {} user contracts", + self.available_contracts.len() + ); + } + + fn refresh_document_types(&mut self, app_context: &AppContext, contract_id: &str) { + self.available_document_types.clear(); + self.selected_document_type = None; + + if let Ok(contracts) = app_context.get_contracts(None, None) { + for contract in contracts { + let id = contract + .contract + .id() + .to_string(dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58); + + if id == contract_id { + self.available_document_types = contract + .contract + .document_types() + .keys() + .map(|s| s.to_string()) + .collect(); + + tracing::info!( + "Found {} document types for contract {}: {:?}", + self.available_document_types.len(), + &contract_id[..8], + self.available_document_types + ); + + break; + } + } + } + } + + fn generate_proof(&mut self, app_context: &AppContext) -> AppAction { + if cfg!(debug_assertions) { + self.gen_error_message = Some( + "GroveSTARK proof generation requires a release build (cargo run --release)." + .to_string(), + ); + self.is_generating = false; + return AppAction::None; + } + + // Reset any prior messages/results before starting a new generation + self.is_generating = true; + self.gen_error_message = None; + self.generated_proof = None; + self.proof_size = None; + self.generation_time = None; + + // Get the required IDs + let identity_id = match &self.selected_identity { + Some(id) => { + // Debug: Log the identity ID being used + tracing::info!( + "ZK Proof generation: Using identity ID: '{}' (length: {})", + id, + id.len() + ); + id.clone() + } + None => { + self.gen_error_message = Some("No identity selected".to_string()); + self.is_generating = false; + return AppAction::None; + } + }; + + let selected_key = match &self.selected_key { + Some(key) => key, + None => { + self.gen_error_message = Some("No key selected".to_string()); + self.is_generating = false; + return AppAction::None; + } + }; + + let contract_id = match &self.selected_contract { + Some(id) => { + tracing::info!( + "ZK Proof generation: Using contract ID: '{}' (length: {})", + id, + id.len() + ); + id.clone() + } + None => { + self.gen_error_message = Some("No contract selected".to_string()); + self.is_generating = false; + return AppAction::None; + } + }; + + let document_type = match &self.selected_document_type { + Some(doc_type) => { + tracing::info!("ZK Proof generation: Using document type: '{}'", doc_type); + doc_type.clone() + } + None => { + self.gen_error_message = Some("No document type selected".to_string()); + self.is_generating = false; + return AppAction::None; + } + }; + + let document_id = match &self.selected_document { + Some(id) => { + tracing::info!( + "ZK Proof generation: Using document ID: '{}' (length: {})", + id, + id.len() + ); + id.clone() + } + None => { + self.gen_error_message = Some("No document selected".to_string()); + self.is_generating = false; + return AppAction::None; + } + }; + + // Get the private key from the qualified identity + let private_key = match self.get_qualified_identity(&identity_id) { + Some(qualified_identity) => { + // Get the wallets for resolving encrypted keys + let wallets = app_context.wallets.read().unwrap(); + let wallet_vec: Vec<_> = wallets.values().cloned().collect(); + + // Try to get the private key + match qualified_identity.private_keys.get_resolve( + &( + PrivateKeyTarget::PrivateKeyOnMainIdentity, + selected_key.id(), + ), + &wallet_vec, + app_context.network, + ) { + Ok(Some((_, private_key_bytes))) => private_key_bytes, + Ok(None) => { + self.gen_error_message = + Some("Private key not found in storage".to_string()); + self.is_generating = false; + return AppAction::None; + } + Err(e) => { + self.gen_error_message = Some(format!("Failed to get private key: {}", e)); + self.is_generating = false; + return AppAction::None; + } + } + } + None => { + self.gen_error_message = Some("Qualified identity not found".to_string()); + self.is_generating = false; + return AppAction::None; + } + }; + + // For EDDSA_25519_HASH160, the key data is only 20 bytes (the hash) + // We need to derive the public key from the private key + let public_key = { + use ed25519_dalek::SigningKey; + let signing_key = SigningKey::from_bytes(&private_key); + let verifying_key = signing_key.verifying_key(); + *verifying_key.as_bytes() + }; + + // Use fixed parameters for simplicity and consistency + let task = BackendTask::GroveSTARKTask(GroveSTARKTask::GenerateProof { + identity_id, + contract_id, + document_type, + document_id, + key_id: selected_key.id(), + private_key, + public_key, + }); + + AppAction::BackendTask(task) + } + + fn verify_proof(&mut self, _app_context: &AppContext) -> AppAction { + if cfg!(debug_assertions) { + self.verify_error_message = Some( + "GroveSTARK proof verification requires a release build (cargo run --release)." + .to_string(), + ); + self.is_verifying = false; + return AppAction::None; + } + + self.is_verifying = true; + self.verify_error_message = None; + self.verification_result = None; // Clear any previous results + + // Parse the proof from pasted text + let proof_result = + // Try to parse from base64-encoded JSON first, then raw JSON + crate::model::grovestark_prover::ProofDataOutput::from_base64( + &self.proof_text, + ) + .or_else(|_| { + crate::model::grovestark_prover::ProofDataOutput::from_json_string( + &self.proof_text, + ) + }); + + match proof_result { + Ok(proof_data) => { + let task = BackendTask::GroveSTARKTask(GroveSTARKTask::VerifyProof { proof_data }); + AppAction::BackendTask(task) + } + Err(e) => { + self.verify_error_message = Some(format!("Failed to parse proof: {}", e)); + self.is_verifying = false; + AppAction::None + } + } + } + + fn copy_proof_to_clipboard(&self) { + if let Some(proof) = &self.generated_proof { + // Use the helper method to serialize to base64 + if let Ok(proof_base64) = proof.full_proof.to_base64() { + let _ = arboard::Clipboard::new() + .and_then(|mut clipboard| clipboard.set_text(proof_base64)); + } + } + } + + fn copy_verification_result(&self) { + if let Some(result) = &self.verification_result { + let text = format!( + "Verification Result: {}\nContract: {}\nSecurity Level: {}-bit", + if result.is_valid { "VALID" } else { "INVALID" }, + result.contract_id, + result.security_level + ); + let _ = arboard::Clipboard::new().and_then(|mut clipboard| clipboard.set_text(text)); + } + } + + fn truncate_id(id: &str) -> String { + if id.len() > 16 { + format!("{}...{}", &id[..6], &id[id.len() - 6..]) + } else { + id.to_string() + } + } + + fn format_timestamp(timestamp: u64) -> String { + chrono::DateTime::from_timestamp(timestamp as i64, 0) + .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()) + .unwrap_or_else(|| "Unknown".to_string()) + } + + fn render_generation_ui(&mut self, ui: &mut Ui, app_context: &AppContext) -> Option { + let dark_mode = ui.ctx().style().visuals.dark_mode; + let debug_build = cfg!(debug_assertions); + + ui.label( + RichText::new("Contract Membership Circuit") + .size(Typography::SCALE_XL) + .strong() + .color(DashColors::text_primary(dark_mode)), + ); + ui.label( + RichText::new("Prove you own a document in a specific contract without revealing anything about your identity or the document.") + .size(Typography::SCALE_SM) + .color(DashColors::text_primary(dark_mode)) + ); + ui.add_space(Spacing::SM); + ui.separator(); + + if debug_build { + ui.colored_label( + egui::Color32::DARK_RED, + "GroveSTARK proofs require a release build (cargo run --release).", + ); + ui.add_space(Spacing::SM); + } + + // Step 1: Select Identity + Frame::new() + .inner_margin(Margin::same(Spacing::MD_I8)) + .fill(DashColors::surface(dark_mode)) + .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .show(ui, |ui| { + ui.label( + RichText::new("Step 1: Select Identity") + .size(Typography::SCALE_LG) + .strong() + .color(DashColors::text_primary(dark_mode)), + ); + ui.horizontal(|ui| { + ui.label("Identity:"); + let mut identity_changed = false; + ComboBox::from_id_salt("identity_selector") + .selected_text(self.selected_identity.as_deref().unwrap_or( + if self.available_identities.is_empty() { + "No identities available" + } else { + "Select..." + }, + )) + .show_ui(ui, |ui| { + if self.available_identities.is_empty() { + ui.label("No identities with EdDSA keys found."); + ui.label( + RichText::new("ZK proofs require identities with EdDSA (Ed25519) keys. Please add an EdDSA key to an identity.") + .size(Typography::SCALE_XS) + .color(DashColors::text_secondary(dark_mode)) + ); + } else { + for identity in &self.available_identities { + let id_str = identity.id().to_string(Encoding::Base58); + if ui + .selectable_value( + &mut self.selected_identity, + Some(id_str.clone()), + Self::truncate_id(&id_str), + ) + .changed() + { + identity_changed = true; + } + } + } + }); + + // Reset key selection if identity changed + if identity_changed { + self.selected_key = None; + } + }); + + if let Some(id) = &self.selected_identity { + ui.label( + RichText::new("✅ Identity selected").color(egui::Color32::DARK_GREEN), + ); + + // Key selection + ui.separator(); + ui.label( + RichText::new("Select Key for Signing:") + .color(DashColors::text_primary(dark_mode)), + ); + + let available_keys: Vec = + self.get_available_keys(id).into_iter().cloned().collect(); + + if available_keys.is_empty() { + ui.label( + RichText::new("⚠️ No EdDSA keys available for ZK proof generation") + .color(egui::Color32::DARK_RED), + ); + ui.label( + RichText::new("ZK proofs require EdDSA (Ed25519) keys. Please add an EdDSA key to this identity.") + .size(Typography::SCALE_XS) + .color(DashColors::text_secondary(dark_mode)), + ); + } else { + ComboBox::from_id_salt("key_selector") + .selected_text( + self.selected_key + .as_ref() + .map(|k| { + format!( + "EdDSA Key {} ({} - {})", + k.id(), + k.purpose(), + k.security_level() + ) + }) + .unwrap_or_else(|| "Select key...".to_string()), + ) + .show_ui(ui, |ui| { + for key in &available_keys { + let key_label = format!( + "EdDSA Key {} ({} - {})", + key.id(), + key.purpose(), + key.security_level() + ); + ui.selectable_value( + &mut self.selected_key, + Some(key.clone()), + key_label, + ); + } + }); + + if self.selected_key.is_some() { + ui.label( + RichText::new("✅ EdDSA key selected").color(egui::Color32::DARK_GREEN), + ); + } + } + } + }); + + ui.add_space(Spacing::MD); + + // Step 2: Select Contract + Frame::new() + .inner_margin(Margin::same(Spacing::MD_I8)) + .fill(DashColors::surface(dark_mode)) + .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .show(ui, |ui| { + ui.label( + RichText::new("Step 2: Select Contract") + .size(Typography::SCALE_LG) + .strong() + .color(DashColors::text_primary(dark_mode)), + ); + ui.horizontal(|ui| { + ui.label("Contract:"); + let mut contract_changed = false; + ComboBox::from_id_salt("contract_selector") + .selected_text(self.selected_contract.as_deref().unwrap_or( + if self.available_contracts.is_empty() { + "No contracts available" + } else { + "Select..." + }, + )) + .show_ui(ui, |ui| { + if self.available_contracts.is_empty() { + ui.label( + "No user contracts found. Please create a contract first.", + ); + } else { + for (id, name) in &self.available_contracts { + if ui + .selectable_value( + &mut self.selected_contract, + Some(id.clone()), + name, + ) + .changed() + { + contract_changed = true; + } + } + } + }); + + // If contract changed, refresh document types + if contract_changed && let Some(contract_id) = self.selected_contract.clone() { + self.refresh_document_types(app_context, &contract_id); + } + }); + + if let Some(_contract_id) = &self.selected_contract { + ui.label( + RichText::new("✅ Contract selected").color(egui::Color32::DARK_GREEN), + ); + + // Document Type selection + ui.separator(); + ui.label( + RichText::new("Select Document Type:") + .color(DashColors::text_primary(dark_mode)), + ); + + ui.horizontal(|ui| { + ui.label("Document Type:"); + ComboBox::from_id_salt("document_type_selector") + .selected_text(self.selected_document_type.as_deref().unwrap_or( + if self.available_document_types.is_empty() { + "No document types available" + } else { + "Select..." + }, + )) + .show_ui(ui, |ui| { + if self.available_document_types.is_empty() { + ui.label("No document types found for this contract."); + } else { + for doc_type in &self.available_document_types { + ui.selectable_value( + &mut self.selected_document_type, + Some(doc_type.clone()), + doc_type, + ); + } + } + }); + }); + + if self.selected_document_type.is_some() { + ui.label( + RichText::new("✅ Document type selected") + .color(egui::Color32::DARK_GREEN), + ); + } + } + }); + + ui.add_space(Spacing::MD); + + // Step 3: Select Document + Frame::new() + .inner_margin(Margin::same(Spacing::MD_I8)) + .fill(DashColors::surface(dark_mode)) + .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .show(ui, |ui| { + ui.label( + RichText::new("Step 3: Select Document") + .size(Typography::SCALE_LG) + .strong() + .color(DashColors::text_primary(dark_mode)), + ); + ui.horizontal(|ui| { + ui.label("Document ID:"); + let mut document_id = + self.selected_document.as_deref().unwrap_or("").to_string(); + if ui.text_edit_singleline(&mut document_id).changed() { + self.selected_document = if document_id.is_empty() { + None + } else { + Some(document_id) + }; + } + }); + + if let Some(_doc_id) = &self.selected_document { + ui.label( + RichText::new("✅ Document selected").color(egui::Color32::DARK_GREEN), + ); + } + }); + + // Advanced Options removed to reduce confusion; defaults are used. + + ui.separator(); + + // Generate Button + let can_generate = self.selected_identity.is_some() + && self.selected_key.is_some() + && self.selected_contract.is_some() + && self.selected_document_type.is_some() + && self.selected_document.is_some(); + + let mut action = None; + ui.horizontal(|ui| { + if self.is_generating { + // Use Dash blue spinner instead of default + ui.add(egui::widgets::Spinner::new().color(DashColors::DASH_BLUE)); + ui.vertical(|ui| { + ui.label("Generating ZK proof..."); + }); + } else if ui + .add_enabled( + !debug_build && can_generate, + Button::new("🔐 Generate Proof"), + ) + .clicked() + { + action = Some(self.generate_proof(app_context)); + } + }); + if action.is_some() { + return action; + } + + // Error Display + if let Some(error) = &self.gen_error_message { + ui.colored_label(egui::Color32::RED, format!("Error: {}", error)); + } + + // Success Display + if let Some(_proof) = &self.generated_proof { + ui.separator(); + Frame::new() + .inner_margin(Margin::same(Spacing::MD_I8)) + .fill(DashColors::surface(dark_mode)) + .stroke(egui::Stroke::new(1.0, egui::Color32::DARK_GREEN)) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .show(ui, |ui| { + ui.label( + RichText::new("✅ Proof Generated Successfully!") + .color(egui::Color32::DARK_GREEN) + .strong(), + ); + + if ui.button("📋 Copy Proof").clicked() { + self.copy_proof_to_clipboard(); + } + }); + } + None + } + + fn render_verification_ui( + &mut self, + ui: &mut Ui, + app_context: &AppContext, + ) -> Option { + let dark_mode = ui.ctx().style().visuals.dark_mode; + let debug_build = cfg!(debug_assertions); + + ui.label( + RichText::new("Verify Zero-Knowledge Proof") + .size(Typography::SCALE_XL) + .strong() + .color(DashColors::text_primary(dark_mode)), + ); + ui.add_space(Spacing::SM); + ui.separator(); + + // Proof Input + Frame::new() + .inner_margin(Margin::same(Spacing::MD_I8)) + .fill(DashColors::surface(dark_mode)) + .stroke(egui::Stroke::new(1.0, DashColors::border_light(dark_mode))) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .show(ui, |ui| { + ui.label( + RichText::new("Paste Proof (Base64 or JSON):") + .color(DashColors::text_primary(dark_mode)), + ); + ui.add( + TextEdit::multiline(&mut self.proof_text) + .desired_width(f32::INFINITY) + .desired_rows(6), + ); + }); + + ui.separator(); + + // Error Display (above the button) + if let Some(error) = &self.verify_error_message { + ui.colored_label(egui::Color32::RED, format!("Error: {}", error)); + } + + // Verify Button + let can_verify = !self.proof_text.is_empty(); + + let mut action = None; + ui.horizontal(|ui| { + if self.is_verifying { + // Use Dash blue spinner instead of default + ui.add(egui::widgets::Spinner::new().color(DashColors::DASH_BLUE)); + ui.label("Verifying ZK proof..."); + } else if ui + .add_enabled(!debug_build && can_verify, Button::new("✅ Verify Proof")) + .clicked() + { + action = Some(self.verify_proof(app_context)); + } + }); + if action.is_some() { + return action; + } + + // Verification Result + if let Some(result) = &self.verification_result { + ui.separator(); + + if result.is_valid { + Frame::new() + .inner_margin(Margin::same(Spacing::MD_I8)) + .fill(DashColors::surface(dark_mode)) + .stroke(egui::Stroke::new(1.0, egui::Color32::DARK_GREEN)) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .show(ui, |ui| { + ui.colored_label(egui::Color32::DARK_GREEN, "✅ PROOF IS VALID"); + + Grid::new("verification_details") + .num_columns(2) + .show(ui, |ui| { + ui.label("Verified At:"); + ui.label(Self::format_timestamp(result.verified_at)); + ui.end_row(); + + ui.label("Document Exists:"); + ui.label("Yes"); + ui.end_row(); + + ui.label("Key Control:"); + ui.label("Verified"); + ui.end_row(); + + ui.label("Contract:"); + ui.label(&result.contract_id); + ui.end_row(); + + ui.label("Security Level:"); + ui.label(format!("{}-bit", result.security_level)); + ui.end_row(); + }); + + if ui.button("📋 Copy Result").clicked() { + self.copy_verification_result(); + } + }); + } else { + Frame::new() + .inner_margin(Margin::same(Spacing::MD_I8)) + .fill(DashColors::surface(dark_mode)) + .stroke(egui::Stroke::new(1.0, egui::Color32::RED)) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .show(ui, |ui| { + ui.colored_label(egui::Color32::RED, "❌ PROOF IS INVALID"); + if let Some(reason) = &result.error_message { + ui.label(format!("Reason: {}", reason)); + } + + ui.collapsing("Technical Details", |ui| { + ui.monospace(&result.technical_details); + }); + }); + } + } + None + } +} + +impl ScreenLike for GroveSTARKScreen { + fn refresh(&mut self) { + // Refresh implementation if needed + } + + fn refresh_on_arrival(&mut self) { + self.refresh(); + // Reload data in case it changed + let app_context = self.app_context.clone(); + self.refresh_identities(&app_context); + self.refresh_contracts(&app_context); + } + + fn display_message(&mut self, message: &str, message_type: crate::ui::MessageType) { + // Only record errors and scope them to the active mode + if message_type == crate::ui::MessageType::Error { + match self.mode { + ProofMode::Generate => self.gen_error_message = Some(message.to_string()), + ProofMode::Verify => self.verify_error_message = Some(message.to_string()), + } + self.is_generating = false; + self.is_verifying = false; + } + } + + fn display_task_result( + &mut self, + backend_task_success_result: crate::backend_task::BackendTaskSuccessResult, + ) { + use crate::backend_task::BackendTaskSuccessResult; + + match backend_task_success_result { + BackendTaskSuccessResult::GeneratedZKProof(proof_data) => { + self.is_generating = false; + let proof_size = proof_data.proof.len(); + self.generated_proof = Some(ProofData { + full_proof: proof_data.clone(), + hash: hex::encode(&proof_data.public_inputs.state_root[0..8]), + size: proof_size, + generation_time: std::time::Duration::from_millis( + proof_data.metadata.generation_time_ms, + ), + }); + self.proof_size = Some(format!("{} bytes", proof_data.metadata.proof_size)); + self.generation_time = Some(std::time::Duration::from_millis( + proof_data.metadata.generation_time_ms, + )); + self.gen_error_message = None; + } + BackendTaskSuccessResult::VerifiedZKProof(is_valid, proof_data) => { + self.is_verifying = false; + // Get contract ID from the proof data itself + let contract_id = hex::encode(proof_data.public_inputs.contract_id); + self.verification_result = Some(VerificationResult { + is_valid, + verified_at: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + contract_id, + security_level: self.security_level, + error_message: if !is_valid { + Some("Proof verification failed".to_string()) + } else { + None + }, + technical_details: format!( + "Verification result: {}", + if is_valid { "VALID" } else { "INVALID" } + ), + }); + self.verify_error_message = None; + } + _ => {} + } + } + + fn pop_on_success(&mut self) { + // Pop on success if needed + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = AppAction::None; + + // Add top panel with breadcrumb + action |= add_top_panel( + ctx, + &self.app_context, + vec![("Tools", AppAction::None)], + vec![], + ); + + // Add left panel + action |= add_left_panel( + ctx, + &self.app_context, + RootScreenType::RootScreenToolsGroveSTARKScreen, + ); + + // Add tools subscreen chooser panel + action |= add_tools_subscreen_chooser_panel(ctx, self.app_context.as_ref()); + + // Add central panel with the main UI + let panel_action = island_central_panel(ctx, |ui| { + ui.label( + RichText::new("GroveSTARK Zero-Knowledge Proofs") + .size(Typography::SCALE_XL) + .strong() + .color(DashColors::text_primary(ui.ctx().style().visuals.dark_mode)), + ); + ui.add_space(5.0); + + // Add research warning + ui.label( + RichText::new("WARNING: GroveSTARK is a research project. It has not been audited and may contain bugs and security flaws. This feature is NOT ready for production usage.") + .size(Typography::SCALE_XS) + .color(DashColors::text_primary(ui.ctx().style().visuals.dark_mode)) + ); + ui.add_space(Spacing::SM); + ui.separator(); + + let mut content_action = AppAction::None; + let available_height = ui.available_height(); + + // Mode Toggle at the top + ui.horizontal(|ui| { + ui.label( + RichText::new("Mode:") + .size(Typography::SCALE_LG) + .strong() + .color(DashColors::text_primary(ui.ctx().style().visuals.dark_mode)), + ); + ui.add_space(10.0); + + let dark_mode = ui.ctx().style().visuals.dark_mode; + + // Generate button + let generate_selected = self.mode == ProofMode::Generate; + let generate_button = if generate_selected { + Button::new( + RichText::new("🔐 Generate Proof") + .color(DashColors::WHITE) + .size(Typography::SCALE_SM), + ) + .fill(DashColors::DASH_BLUE) + .stroke(egui::Stroke::NONE) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .min_size(egui::Vec2::new(150.0, 28.0)) + } else { + Button::new( + RichText::new("🔐 Generate Proof") + .color(DashColors::text_primary(dark_mode)) + .size(Typography::SCALE_SM), + ) + .fill(DashColors::glass_white(dark_mode)) + .stroke(egui::Stroke::new(1.0, DashColors::border(dark_mode))) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .min_size(egui::Vec2::new(150.0, 28.0)) + }; + + if ui.add(generate_button).clicked() { + self.mode = ProofMode::Generate; + } + + ui.add_space(5.0); + + // Verify button + let verify_selected = self.mode == ProofMode::Verify; + let verify_button = if verify_selected { + Button::new( + RichText::new("✅ Verify Proof") + .color(DashColors::WHITE) + .size(Typography::SCALE_SM), + ) + .fill(DashColors::DASH_BLUE) + .stroke(egui::Stroke::NONE) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .min_size(egui::Vec2::new(150.0, 28.0)) + } else { + Button::new( + RichText::new("✅ Verify Proof") + .color(DashColors::text_primary(dark_mode)) + .size(Typography::SCALE_SM), + ) + .fill(DashColors::glass_white(dark_mode)) + .stroke(egui::Stroke::new(1.0, DashColors::border(dark_mode))) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_MD)) + .min_size(egui::Vec2::new(150.0, 28.0)) + }; + + if ui.add(verify_button).clicked() { + self.mode = ProofMode::Verify; + } + }); + + ui.separator(); + ui.add_space(Spacing::SM); + + // Main content area with scrolling + ScrollArea::vertical() + .max_height(available_height - 100.0) // Reserve space for mode toggle and margins + .show(ui, |ui| { + // Clone app_context to avoid borrowing issues + let app_context = self.app_context.clone(); + // Render the appropriate UI based on mode + let maybe_action = match self.mode { + ProofMode::Generate => self.render_generation_ui(ui, &app_context), + ProofMode::Verify => self.render_verification_ui(ui, &app_context), + }; + if let Some(ui_action) = maybe_action { + content_action |= ui_action; + } + }); + + content_action + }); + + action |= panel_action; + + // Note: Confirmation dialog handling would be done within the UI context if needed + + action + } +} diff --git a/src/ui/tools/masternode_list_diff_screen.rs b/src/ui/tools/masternode_list_diff_screen.rs new file mode 100644 index 000000000..713b00c21 --- /dev/null +++ b/src/ui/tools/masternode_list_diff_screen.rs @@ -0,0 +1,4407 @@ +use crate::app::AppAction; +use crate::backend_task::core::CoreItem; +use crate::backend_task::mnlist::MnListTask; +use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; +use crate::components::core_p2p_handler::CoreP2PHandler; +use crate::context::AppContext; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::styled::island_central_panel; +use crate::ui::components::tools_subscreen_chooser_panel::add_tools_subscreen_chooser_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::{MessageType, RootScreenType, ScreenLike}; +use dash_sdk::dashcore_rpc::RpcApi; +use dash_sdk::dashcore_rpc::json::QuorumType; +use dash_sdk::dpp::dashcore::bls_sig_utils::BLSSignature; +use dash_sdk::dpp::dashcore::consensus::serialize as serialize2; +use dash_sdk::dpp::dashcore::consensus::{Decodable, deserialize, serialize}; +use dash_sdk::dpp::dashcore::hashes::Hash; +use dash_sdk::dpp::dashcore::network::constants::NetworkExt; +use dash_sdk::dpp::dashcore::network::message_qrinfo::{QRInfo, QuorumSnapshot}; +use dash_sdk::dpp::dashcore::network::message_sml::MnListDiff; +use dash_sdk::dpp::dashcore::sml::llmq_entry_verification::LLMQEntryVerificationStatus; +use dash_sdk::dpp::dashcore::sml::llmq_type::LLMQType; +use dash_sdk::dpp::dashcore::sml::masternode_list::MasternodeList; +use dash_sdk::dpp::dashcore::sml::masternode_list_engine::{ + MasternodeListEngine, MasternodeListEngineBlockContainer, +}; +use dash_sdk::dpp::dashcore::sml::masternode_list_entry::EntryMasternodeType; +use dash_sdk::dpp::dashcore::sml::masternode_list_entry::qualified_masternode_list_entry::QualifiedMasternodeListEntry; +use dash_sdk::dpp::dashcore::sml::quorum_entry::qualified_quorum_entry::{ + QualifiedQuorumEntry, VerifyingChainLockSignaturesType, +}; +use dash_sdk::dpp::dashcore::sml::quorum_validation_error::ClientDataRetrievalError; +use dash_sdk::dpp::dashcore::transaction::special_transaction::quorum_commitment::QuorumEntry; +use dash_sdk::dpp::dashcore::{ + Block, BlockHash as BlockHash2, ChainLock, InstantLock, Transaction, +}; +use dash_sdk::dpp::dashcore::{ + BlockHash, ChainLock as ChainLock2, InstantLock as InstantLock2, Network, ProTxHash, QuorumHash, +}; +use dash_sdk::dpp::prelude::CoreBlockHeight; +use eframe::egui::{self, Context, ScrollArea, Ui}; +use egui::{Align, Color32, Frame, Layout, Margin, RichText, Stroke, TextEdit, Vec2}; +use itertools::Itertools; +use rfd::FileDialog; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::fs; +use std::path::Path; +use std::sync::Arc; + +type HeightHash = (u32, BlockHash); + +enum SelectedQRItem { + SelectedSnapshot(QuorumSnapshot), + MNListDiff(Box), + QuorumEntry(Box), +} + +/// Screen for viewing MNList diffs (diffs in the masternode list and quorums) +pub struct MasternodeListDiffScreen { + pub app_context: Arc, + + /// Are we syncing? + syncing: bool, + + /// The chain locked blocks received through zmq that we can attempt to verify + chain_locked_blocks: BTreeMap, + + /// Instant send locked transactions received through zmq that we can attempt to verify + instant_send_transactions: Vec<(Transaction, InstantLock, bool)>, + + /// The user‐entered base block height (as text) + base_block_height: String, + /// The user‐entered end block height (as text) + end_block_height: String, + + show_popup_for_render_masternode_list_engine: bool, + + /// Selected tab (0 = Diffs, 1 = Masternode Lists) + selected_tab: usize, + + /// The engine to compute masternode lists + masternode_list_engine: MasternodeListEngine, + + /// Masternode_list_heights with all quorum heights known + masternode_lists_with_all_quorum_heights_known: BTreeSet, + + /// The list of MNList diff items (one per block height) + mnlist_diffs: BTreeMap<(CoreBlockHeight, CoreBlockHeight), MnListDiff>, + + /// The list of qr infos + qr_infos: BTreeMap, + + /// Selected MNList diff + selected_dml_diff_key: Option<(CoreBlockHeight, CoreBlockHeight)>, + + /// This is to know which ones we have already checked for quorum heights + dml_diffs_with_cached_quorum_heights: HashSet<(CoreBlockHeight, CoreBlockHeight)>, + + /// Selected MNList + selected_dml_height_key: Option, + + /// Selected display option + selected_option_index: Option, + /// Selected quorum within the MNList diff + selected_quorum_in_diff_index: Option, + + /// Selected masternode within the MNList diff + selected_masternode_in_diff_index: Option, + + /// Selected quorum within the MNList diff + selected_quorum_hash_in_mnlist_diff: Option<(LLMQType, QuorumHash)>, + + /// Selected quorum within the quorum_viewer + selected_quorum_type_in_quorum_viewer: Option, + + /// Selected quorum within the quorum_viewer + selected_quorum_hash_in_quorum_viewer: Option, + + /// Selected masternode within the MNList diff + selected_masternode_pro_tx_hash: Option, + + /// Search term + search_term: Option, + + /// The block height cache + block_height_cache: BTreeMap, + + /// The block hash cache + block_hash_cache: BTreeMap, + + /// The masternode list quorum hash cache + masternode_list_quorum_hash_cache: + BTreeMap>>, + + chain_lock_sig_cache: BTreeMap<(CoreBlockHeight, BlockHash), Option>, + + chain_lock_reversed_sig_cache: BTreeMap>, + + error: Option, + selected_qr_field: Option, + selected_qr_list_index: Option, + selected_core_item: Option<(CoreItem, bool)>, + selected_qr_item: Option, + pending: Option, + queued_task: Option, + message: Option<(String, MessageType)>, +} + +impl MasternodeListDiffScreen { + /// Create a new MNListDiffScreen + pub fn new(app_context: &Arc) -> Self { + let mut mnlist_diffs = BTreeMap::new(); + let engine = match app_context.network { + Network::Dash => { + use std::env; + println!( + "Current working directory: {:?}", + env::current_dir().unwrap() + ); + let file_path = "artifacts/mn_list_diff_0_2227096.bin"; + // Attempt to load and parse the MNListDiff file + if Path::new(file_path).exists() { + match fs::read(file_path) { + Ok(bytes) => { + let diff: MnListDiff = + deserialize(bytes.as_slice()).expect("expected to deserialize"); + mnlist_diffs.insert((0, 2227096), diff.clone()); + MasternodeListEngine::initialize_with_diff_to_height( + diff, + 2227096, + Network::Dash, + ) + .expect("expected to start engine") + } + Err(e) => { + eprintln!("Failed to read MNListDiff file: {}", e); + MasternodeListEngine::default_for_network(Network::Dash) + } + } + } else { + eprintln!("MNListDiff file not found: {}", file_path); + MasternodeListEngine::default_for_network(Network::Dash) + } + } + Network::Testnet => { + let file_path = "artifacts/mn_list_diff_testnet_0_1296600.bin"; + // Attempt to load and parse the MNListDiff file + if Path::new(file_path).exists() { + match fs::read(file_path) { + Ok(bytes) => { + let diff: MnListDiff = + deserialize(bytes.as_slice()).expect("expected to deserialize"); + mnlist_diffs.insert((0, 1296600), diff.clone()); + MasternodeListEngine::initialize_with_diff_to_height( + diff, + 1296600, + Network::Testnet, + ) + .expect("expected to start engine") + } + Err(e) => { + eprintln!("Failed to read MNListDiff file: {}", e); + MasternodeListEngine::default_for_network(Network::Testnet) + } + } + } else { + eprintln!("MNListDiff file not found: {}", file_path); + MasternodeListEngine::default_for_network(Network::Dash) + } + } + _ => MasternodeListEngine::default_for_network(app_context.network), + }; + + Self { + app_context: app_context.clone(), + syncing: false, + chain_locked_blocks: Default::default(), + instant_send_transactions: vec![], + base_block_height: "".to_string(), + end_block_height: "".to_string(), + show_popup_for_render_masternode_list_engine: false, + selected_tab: 0, + masternode_list_engine: engine, + search_term: None, + mnlist_diffs, + qr_infos: Default::default(), + selected_dml_diff_key: None, + dml_diffs_with_cached_quorum_heights: Default::default(), + selected_dml_height_key: None, + selected_option_index: None, + selected_quorum_in_diff_index: None, + selected_masternode_in_diff_index: None, + selected_quorum_hash_in_mnlist_diff: None, + selected_quorum_type_in_quorum_viewer: None, + selected_quorum_hash_in_quorum_viewer: None, + selected_masternode_pro_tx_hash: None, + error: None, + selected_qr_field: None, + selected_qr_list_index: None, + block_height_cache: Default::default(), + block_hash_cache: Default::default(), + masternode_list_quorum_hash_cache: Default::default(), + selected_qr_item: None, + selected_core_item: None, + masternode_lists_with_all_quorum_heights_known: Default::default(), + chain_lock_sig_cache: Default::default(), + chain_lock_reversed_sig_cache: Default::default(), + pending: None, + queued_task: None, + message: None, + } + } + + fn get_height_or_error_as_string(&self, block_hash: &BlockHash) -> String { + match self.get_height(block_hash) { + Ok(height) => height.to_string(), + Err(e) => format!("Failed to get height for {}: {}", block_hash, e), + } + } + + /// Build a backend task that fetches the extra diffs needed to validate non-rotating quorums. + /// Returns None if requirements cannot be computed. + fn build_validation_diffs_task(&mut self) -> Option { + // Determine hashes we need to validate + let hashes = self + .masternode_list_engine + .latest_masternode_list_non_rotating_quorum_hashes( + &[LLMQType::Llmqtype50_60, LLMQType::Llmqtype400_85], + true, + ); + if hashes.is_empty() { + return None; + } + + // Compute target validation heights (h-8) + let mut heights: BTreeSet = BTreeSet::new(); + for quorum_hash in &hashes { + if let Ok(h) = self.get_height_and_cache(quorum_hash) + && h >= 8 + { + heights.insert(h - 8); + } + } + if heights.is_empty() { + return None; + } + + let client = self.app_context.core_client.read().unwrap(); + let mut chain: Vec<(u32, BlockHash, u32, BlockHash)> = Vec::new(); + + // Determine base starting point similar to previous logic + let (first_engine_height, first_engine_hash_opt) = self + .masternode_list_engine + .masternode_lists + .first_key_value() + .map(|(h, l)| (*h, Some(l.block_hash))) + .unwrap_or((0, None)); + + let oldest_needed = *heights.first().unwrap(); + let mut base_height: u32; + let mut base_hash: BlockHash; + if first_engine_height != 0 && first_engine_height < oldest_needed { + base_height = first_engine_height; + base_hash = first_engine_hash_opt.unwrap(); + } else { + // Use genesis as base + base_height = 0; + let Ok(genesis) = client.get_block_hash(0) else { + return None; + }; + base_hash = BlockHash::from_byte_array(genesis.to_byte_array()); + } + + for h in heights { + let Ok(bh) = client.get_block_hash(h) else { + continue; + }; + let bh = BlockHash::from_byte_array(bh.to_byte_array()); + chain.push((base_height, base_hash, h, bh)); + base_height = h; + base_hash = bh; + } + + if chain.is_empty() { + return None; + } + Some(BackendTask::MnListTask(MnListTask::FetchDiffsChain { + chain, + })) + } + + fn get_height(&self, block_hash: &BlockHash) -> Result { + let Some(height) = self + .masternode_list_engine + .block_container + .get_height(block_hash) + else { + let Some(height) = self.block_height_cache.get(block_hash) else { + println!( + "Asking core for height no cache {} ({})", + block_hash, + block_hash.reverse() + ); + return match self + .app_context + .core_client + .read() + .unwrap() + .get_block_header_info( + &(BlockHash2::from_byte_array(block_hash.to_byte_array())), + ) { + Ok(block_hash) => Ok(block_hash.height as CoreBlockHeight), + Err(e) => Err(e.to_string()), + }; + }; + return Ok(*height); + }; + Ok(height) + } + + #[allow(dead_code)] + fn get_height_and_cache_or_error_as_string(&mut self, block_hash: &BlockHash) -> String { + match self.get_height_and_cache(block_hash) { + Ok(height) => height.to_string(), + Err(e) => format!("Failed to get height for {}: {}", block_hash, e), + } + } + + fn get_height_and_cache(&mut self, block_hash: &BlockHash) -> Result { + let Some(height) = self + .masternode_list_engine + .block_container + .get_height(block_hash) + else { + let Some(height) = self.block_height_cache.get(block_hash) else { + println!( + "Asking core for height {} ({})", + block_hash, + block_hash.reverse() + ); + return match self + .app_context + .core_client + .read() + .unwrap() + .get_block_header_info( + &(BlockHash2::from_byte_array(block_hash.to_byte_array())), + ) { + Ok(result) => { + self.block_height_cache + .insert(*block_hash, result.height as CoreBlockHeight); + self.masternode_list_engine + .feed_block_height(result.height as CoreBlockHeight, *block_hash); + Ok(result.height as CoreBlockHeight) + } + Err(e) => Err(e.to_string()), + }; + }; + return Ok(*height); + }; + Ok(height) + } + + #[allow(dead_code)] + fn get_chain_lock_sig_and_cache( + &mut self, + block_hash: &BlockHash, + ) -> Result, String> { + let height = self.get_height_and_cache(block_hash)?; + if !self + .chain_lock_sig_cache + .contains_key(&(height, *block_hash)) + { + let block = self + .app_context + .core_client + .read() + .unwrap() + .get_block(&(BlockHash2::from_byte_array(block_hash.to_byte_array()))) + .map_err(|e| e.to_string())?; + let Some(coinbase) = block + .coinbase() + .and_then(|coinbase| coinbase.special_transaction_payload.as_ref()) + .and_then(|payload| payload.clone().to_coinbase_payload().ok()) + else { + return Err(format!("coinbase not found on block hash {}", block_hash)); + }; + //todo clean up + self.chain_lock_sig_cache.insert( + (height, *block_hash), + coinbase.best_cl_signature.map(|sig| sig.to_bytes().into()), + ); + if let Some(sig) = coinbase.best_cl_signature.map(|sig| sig.to_bytes().into()) { + self.chain_lock_reversed_sig_cache + .entry(sig) + .or_default() + .insert((height, *block_hash)); + } + } + + Ok(*self + .chain_lock_sig_cache + .get(&(height, *block_hash)) + .unwrap()) + } + + fn get_chain_lock_sig(&self, block_hash: &BlockHash) -> Result, String> { + let height = self.get_height(block_hash)?; + if !self + .chain_lock_sig_cache + .contains_key(&(height, *block_hash)) + { + let block = self + .app_context + .core_client + .read() + .unwrap() + .get_block(&(BlockHash2::from_byte_array(block_hash.to_byte_array()))) + .map_err(|e| e.to_string())?; + let Some(coinbase) = block + .coinbase() + .and_then(|coinbase| coinbase.special_transaction_payload.as_ref()) + .and_then(|payload| payload.clone().to_coinbase_payload().ok()) + else { + return Err(format!("coinbase not found on block hash {}", block_hash)); + }; + Ok(coinbase.best_cl_signature.map(|sig| sig.to_bytes().into())) + } else { + Ok(*self + .chain_lock_sig_cache + .get(&(height, *block_hash)) + .unwrap()) + } + } + + fn get_block_hash(&self, height: CoreBlockHeight) -> Result { + let Some(block_hash) = self + .masternode_list_engine + .block_container + .get_hash(&height) + else { + let Some(block_hash) = self.block_hash_cache.get(&height) else { + // println!("Asking core for hash of {}", height); + return match self + .app_context + .core_client + .read() + .unwrap() + .get_block_hash(height) + { + Ok(block_hash) => Ok(BlockHash::from_byte_array(block_hash.to_byte_array())), + Err(e) => Err(e.to_string()), + }; + }; + return Ok(*block_hash); + }; + Ok(*block_hash) + } + + #[allow(dead_code)] + fn get_block_hash_and_cache(&mut self, height: CoreBlockHeight) -> Result { + // First, try to get the hash from masternode_list_engine's block_container. + if let Some(block_hash) = self + .masternode_list_engine + .block_container + .get_hash(&height) + { + return Ok(*block_hash); + } + + // Then, check the cache. + if let Some(cached_hash) = self.block_hash_cache.get(&height) { + return Ok(*cached_hash); + } + + // If not cached, retrieve from core client and insert into cache. + // println!("Asking core for hash of {} and caching it", height); + match self + .app_context + .core_client + .read() + .unwrap() + .get_block_hash(height) + { + Ok(core_block_hash) => { + let block_hash = BlockHash::from_byte_array(core_block_hash.to_byte_array()); + self.block_hash_cache.insert(height, block_hash); + Ok(block_hash) + } + Err(e) => Err(e.to_string()), + } + } + // + // fn feed_qr_info_cl_sigs(&mut self, qr_info: &QRInfo) { + // let heights = match self.masternode_list_engine.required_cl_sig_heights(qr_info) { + // Ok(heights) => heights, + // Err(e) => { + // self.error = Some(e.to_string()); + // return; + // } + // }; + // for height in heights { + // let block_hash = match self.get_block_hash(height) { + // Ok(block_hash) => block_hash, + // Err(e) => { + // self.error = Some(e.to_string()); + // return; + // } + // }; + // let maybe_chain_lock_sig = match self + // .app_context + // .core_client + // .get_block(&(BlockHash2::from_byte_array(block_hash.to_byte_array()))) + // { + // Ok(block) => { + // let Some(coinbase) = block + // .coinbase() + // .and_then(|coinbase| coinbase.special_transaction_payload.as_ref()) + // .and_then(|payload| payload.clone().to_coinbase_payload().ok()) + // else { + // self.error = + // Some(format!("coinbase not found on block hash {}", block_hash)); + // return; + // }; + // coinbase.best_cl_signature + // } + // Err(e) => { + // self.error = Some(e.to_string()); + // return; + // } + // }; + // if let Some(maybe_chain_lock_sig) = maybe_chain_lock_sig { + // self.masternode_list_engine.feed_chain_lock_sig( + // block_hash, + // BLSSignature::from(maybe_chain_lock_sig.to_bytes()), + // ); + // } + // } + // } + + #[allow(dead_code)] + fn feed_qr_info_block_heights(&mut self, qr_info: &QRInfo) { + let mn_list_diffs = [ + &qr_info.mn_list_diff_tip, + &qr_info.mn_list_diff_h, + &qr_info.mn_list_diff_at_h_minus_c, + &qr_info.mn_list_diff_at_h_minus_2c, + &qr_info.mn_list_diff_at_h_minus_3c, + ]; + + // If h-4c exists, add it to the list + if let Some((_, mn_list_diff_h_minus_4c)) = + &qr_info.quorum_snapshot_and_mn_list_diff_at_h_minus_4c + { + mn_list_diffs.iter().for_each(|&mn_list_diff| { + self.feed_mn_list_diff_heights(mn_list_diff); + }); + + // Feed h-4c separately + self.feed_mn_list_diff_heights(mn_list_diff_h_minus_4c); + } else { + mn_list_diffs.iter().for_each(|&mn_list_diff| { + self.feed_mn_list_diff_heights(mn_list_diff); + }); + } + + // Process `last_commitment_per_index` quorum hashes + qr_info + .last_commitment_per_index + .iter() + .for_each(|quorum_entry| { + self.feed_quorum_entry_height(quorum_entry); + }); + + // Process `mn_list_diff_list` (extra diffs) + qr_info.mn_list_diff_list.iter().for_each(|mn_list_diff| { + self.feed_mn_list_diff_heights(mn_list_diff); + }); + } + + /// **Helper function:** Feeds the base and block hash heights of an `MnListDiff` + fn feed_mn_list_diff_heights(&mut self, mn_list_diff: &MnListDiff) { + // Feed base block hash height + if let Ok(base_height) = self.get_height(&mn_list_diff.base_block_hash) { + println!("feeding {} {}", base_height, mn_list_diff.base_block_hash); + self.masternode_list_engine + .feed_block_height(base_height, mn_list_diff.base_block_hash); + } else { + self.error = Some(format!( + "Failed to get height for base block hash: {}", + mn_list_diff.base_block_hash + )); + } + + // Feed block hash height + if let Ok(block_height) = self.get_height(&mn_list_diff.block_hash) { + println!("feeding {} {}", block_height, mn_list_diff.block_hash); + self.masternode_list_engine + .feed_block_height(block_height, mn_list_diff.block_hash); + } else { + self.error = Some(format!( + "Failed to get height for block hash: {}", + mn_list_diff.block_hash + )); + } + } + + /// **Helper function:** Feeds the quorum hash height of a `QuorumEntry` + fn feed_quorum_entry_height(&mut self, quorum_entry: &QuorumEntry) { + if let Ok(height) = self.get_height(&quorum_entry.quorum_hash) { + self.masternode_list_engine + .feed_block_height(height, quorum_entry.quorum_hash); + } else { + self.error = Some(format!( + "Failed to get height for quorum hash: {}", + quorum_entry.quorum_hash + )); + } + } + + fn parse_heights(&mut self) -> Result<(HeightHash, HeightHash), String> { + let base = if self.base_block_height.is_empty() { + self.base_block_height = "0".to_string(); + match self + .app_context + .core_client + .read() + .unwrap() + .get_block_hash(0) + { + Ok(block_hash) => (0, BlockHash::from_byte_array(block_hash.to_byte_array())), + Err(e) => { + return Err(e.to_string()); + } + } + } else { + match self.base_block_height.trim().parse() { + Ok(start) => match self + .app_context + .core_client + .read() + .unwrap() + .get_block_hash(start) + { + Ok(block_hash) => ( + start, + BlockHash::from_byte_array(block_hash.to_byte_array()), + ), + Err(e) => { + return Err(e.to_string()); + } + }, + Err(e) => { + return Err(e.to_string()); + } + } + }; + let end = if self.end_block_height.is_empty() { + match self + .app_context + .core_client + .read() + .unwrap() + .get_best_block_hash() + { + Ok(block_hash) => { + match self + .app_context + .core_client + .read() + .unwrap() + .get_block_header_info(&block_hash) + { + Ok(header) => { + self.end_block_height = format!("{}", header.height); + ( + header.height as u32, + BlockHash::from_byte_array(block_hash.to_byte_array()), + ) + } + Err(e) => { + return Err(e.to_string()); + } + } + } + Err(e) => { + return Err(e.to_string()); + } + } + } else { + match self.end_block_height.trim().parse() { + Ok(end) => match self + .app_context + .core_client + .read() + .unwrap() + .get_block_hash(end) + { + Ok(block_hash) => (end, BlockHash::from_byte_array(block_hash.to_byte_array())), + Err(e) => { + return Err(e.to_string()); + } + }, + Err(e) => { + return Err(e.to_string()); + } + } + }; + Ok((base, end)) + } + + fn serialize_masternode_list_engine(&self) -> Result { + match bincode::encode_to_vec(&self.masternode_list_engine, bincode::config::standard()) { + Ok(encoded_bytes) => Ok(hex::encode(encoded_bytes)), // Convert to hex string + Err(e) => Err(format!("Serialization failed: {}", e)), + } + } + + fn insert_mn_list_diff(&mut self, mn_list_diff: &MnListDiff) { + let base_block_hash = mn_list_diff.base_block_hash; + let base_height = match self.get_height_and_cache(&base_block_hash) { + Ok(height) => height, + Err(e) => { + self.error = Some(e); + return; + } + }; + let block_hash = mn_list_diff.block_hash; + let height = match self.get_height_and_cache(&block_hash) { + Ok(height) => height, + Err(e) => { + self.error = Some(e); + return; + } + }; + + self.mnlist_diffs + .insert((base_height, height), mn_list_diff.clone()); + } + + fn fetch_rotated_quorum_info( + &mut self, + p2p_handler: &mut CoreP2PHandler, + base_block_hash: BlockHash, + block_hash: BlockHash, + ) -> Option { + let mut known_block_hashes: Vec<_> = self + .mnlist_diffs + .values() + .map(|mn_list_diff| mn_list_diff.block_hash) + .collect(); + known_block_hashes.push(base_block_hash); + println!( + "requesting with known_block_hashes {}", + known_block_hashes + .iter() + .map(|bh| bh.to_string()) + .join(", ") + ); + let qr_info = match p2p_handler.get_qr_info(known_block_hashes, block_hash) { + Ok(list_diff) => list_diff, + Err(e) => { + self.error = Some(e); + return None; + } + }; + self.insert_mn_list_diff(&qr_info.mn_list_diff_tip); + self.insert_mn_list_diff(&qr_info.mn_list_diff_h); + self.insert_mn_list_diff(&qr_info.mn_list_diff_at_h_minus_c); + self.insert_mn_list_diff(&qr_info.mn_list_diff_at_h_minus_2c); + self.insert_mn_list_diff(&qr_info.mn_list_diff_at_h_minus_3c); + if let Some((_, mn_list_diff_at_h_minus_4c)) = + &qr_info.quorum_snapshot_and_mn_list_diff_at_h_minus_4c + { + self.insert_mn_list_diff(mn_list_diff_at_h_minus_4c); + } + for diff in &qr_info.mn_list_diff_list { + self.insert_mn_list_diff(diff) + } + self.qr_infos.insert(block_hash, qr_info.clone()); + Some(qr_info) + } + + fn fetch_diffs_with_hashes( + &mut self, + p2p_handler: &mut CoreP2PHandler, + hashes: BTreeSet, + ) { + let mut hashes_needed_to_validate = BTreeMap::new(); + for quorum_hash in hashes { + let height = match self.get_height_and_cache(&quorum_hash) { + Ok(height) => height, + Err(e) => { + self.error = Some(e.to_string()); + return; + } + }; + let validation_hash = match self + .app_context + .core_client + .read() + .unwrap() + .get_block_hash(height - 8) + { + Ok(block_hash) => block_hash, + Err(e) => { + self.error = Some(e.to_string()); + return; + } + }; + hashes_needed_to_validate.insert( + height - 8, + BlockHash::from_byte_array(validation_hash.to_byte_array()), + ); + } + + if let Some((oldest_needed_height, _)) = hashes_needed_to_validate.first_key_value() { + let (first_engine_height, first_masternode_list) = self + .masternode_list_engine + .masternode_lists + .first_key_value() + .unwrap(); + let (mut base_block_height, mut base_block_hash) = if *first_engine_height + < *oldest_needed_height + { + (*first_engine_height, first_masternode_list.block_hash) + } else { + let known_genesis_block_hash = match self + .masternode_list_engine + .network + .known_genesis_block_hash() + { + None => match self + .app_context + .core_client + .read() + .unwrap() + .get_block_hash(0) + { + Ok(block_hash) => BlockHash::from_byte_array(block_hash.to_byte_array()), + Err(e) => { + self.error = Some(e.to_string()); + return; + } + }, + Some(known_genesis_block_hash) => known_genesis_block_hash, + }; + (0, known_genesis_block_hash) + }; + + for (core_block_height, block_hash) in hashes_needed_to_validate { + self.fetch_single_dml( + p2p_handler, + base_block_hash, + base_block_height, + block_hash, + core_block_height, + false, + ); + base_block_hash = block_hash; + base_block_height = core_block_height; + } + } + } + + fn fetch_single_dml( + &mut self, + p2p_handler: &mut CoreP2PHandler, + base_block_hash: BlockHash, + base_block_height: u32, + block_hash: BlockHash, + block_height: u32, + validate_quorums: bool, + ) { + let list_diff = match p2p_handler.get_dml_diff(base_block_hash, block_hash) { + Ok(list_diff) => list_diff, + Err(e) => { + self.error = Some(e); + return; + } + }; + + if base_block_height == 0 && self.masternode_list_engine.masternode_lists.is_empty() { + self.masternode_list_engine = match MasternodeListEngine::initialize_with_diff_to_height( + list_diff.clone(), + block_height, + self.app_context.network, + ) { + Ok(masternode_list_engine) => masternode_list_engine, + Err(e) => { + self.error = Some(e.to_string()); + return; + } + } + } else if let Err(e) = self.masternode_list_engine.apply_diff( + list_diff.clone(), + Some(block_height), + false, + None, + ) { + self.error = Some(e.to_string()); + return; + } + + if validate_quorums && !self.masternode_list_engine.masternode_lists.is_empty() { + let hashes = self + .masternode_list_engine + .latest_masternode_list_non_rotating_quorum_hashes( + &[LLMQType::Llmqtype50_60, LLMQType::Llmqtype400_85], + true, + ); + self.fetch_diffs_with_hashes(p2p_handler, hashes); + let hashes = self + .masternode_list_engine + .latest_masternode_list_rotating_quorum_hashes(&[]); + for hash in &hashes { + let height = match self.get_height_and_cache(hash) { + Ok(height) => height, + Err(e) => { + self.error = Some(e.to_string()); + return; + } + }; + self.block_height_cache.insert(*hash, height); + } + + if let Err(e) = self + .masternode_list_engine + .verify_non_rotating_masternode_list_quorums( + block_height, + &[LLMQType::Llmqtype50_60, LLMQType::Llmqtype400_85], + ) + { + self.error = Some(e.to_string()); + } + } + + self.mnlist_diffs + .insert((base_block_height, block_height), list_diff); + } + + // fn fetch_range_dml(&mut self, step: u32, include_at_minus_8: bool, count: u32) { + // let ((base_block_height, base_block_hash), (block_height, block_hash)) = + // match self.parse_heights() { + // Ok(a) => a, + // Err(e) => { + // self.error = Some(e); + // return; + // } + // }; + // + // let mut p2p_handler = match CoreP2PHandler::new(self.app_context.network, None) { + // Ok(p2p_handler) => p2p_handler, + // Err(e) => { + // self.error = Some(e); + // return; + // } + // }; + // + // let rem = block_height % 24; + // + // let intermediate_block_height = (block_height - rem).saturating_sub(count * step); + // + // let intermediate_block_hash = match self + // .app_context + // .core_client + // .get_block_hash(intermediate_block_height) + // { + // Ok(block_hash) => BlockHash::from_byte_array(block_hash.to_byte_array()), + // Err(e) => { + // self.error = Some(e.to_string()); + // return; + // } + // }; + // + // self.fetch_single_dml( + // &mut p2p_handler, + // base_block_hash, + // base_block_height, + // intermediate_block_hash, + // intermediate_block_height, + // false, + // ); + // + // let mut last_height = intermediate_block_height; + // let mut last_block_hash = intermediate_block_hash; + // + // for _i in 0..count { + // if include_at_minus_8 { + // let end_height = last_height + step - 8; + // let end_block_hash = match self.app_context.core_client.read().unwrap().get_block_hash(end_height) { + // Ok(block_hash) => BlockHash::from_byte_array(block_hash.to_byte_array()), + // Err(e) => { + // self.error = Some(e.to_string()); + // return; + // } + // }; + // self.fetch_single_dml( + // &mut p2p_handler, + // last_block_hash, + // last_height, + // end_block_hash, + // end_height, + // ); + // last_height = end_height; + // last_block_hash = end_block_hash; + // + // let end_height = last_height + 8; + // let end_block_hash = match self.app_context.core_client.read().unwrap().get_block_hash(end_height) { + // Ok(block_hash) => BlockHash::from_byte_array(block_hash.to_byte_array()), + // Err(e) => { + // self.error = Some(e.to_string()); + // return; + // } + // }; + // self.fetch_single_dml( + // &mut p2p_handler, + // last_block_hash, + // last_height, + // end_block_hash, + // end_height, + // ); + // last_height = end_height; + // last_block_hash = end_block_hash; + // } else { + // let end_height = last_height + step; + // let end_block_hash = match self.app_context.core_client.read().unwrap().get_block_hash(end_height) { + // Ok(block_hash) => BlockHash::from_byte_array(block_hash.to_byte_array()), + // Err(e) => { + // self.error = Some(e.to_string()); + // return; + // } + // }; + // self.fetch_single_dml( + // &mut p2p_handler, + // last_block_hash, + // last_height, + // end_block_hash, + // end_height, + // ); + // last_height = end_height; + // last_block_hash = end_block_hash; + // } + // } + // + // if rem != 0 { + // let end_height = last_height + rem; + // let end_block_hash = match self.app_context.core_client.read().unwrap().get_block_hash(end_height) { + // Ok(block_hash) => BlockHash::from_byte_array(block_hash.to_byte_array()), + // Err(e) => { + // self.error = Some(e.to_string()); + // return; + // } + // }; + // self.fetch_single_dml( + // &mut p2p_handler, + // last_block_hash, + // last_height, + // end_block_hash, + // end_height, + // ); + // } + // + // // Reset selections when new data is loaded + // self.selected_dml_diff_key = None; + // self.selected_quorum_in_diff_index = None; + // } + + /// Clear all data and reset to initial state + pub(crate) fn clear(&mut self) { + self.masternode_list_engine = + MasternodeListEngine::default_for_network(self.app_context.network); + + // Clear cached data structures + self.mnlist_diffs.clear(); + self.qr_infos.clear(); + self.chain_locked_blocks.clear(); + self.instant_send_transactions.clear(); + self.block_height_cache.clear(); + self.block_hash_cache.clear(); + self.masternode_list_quorum_hash_cache.clear(); + self.masternode_lists_with_all_quorum_heights_known.clear(); + self.dml_diffs_with_cached_quorum_heights.clear(); + self.chain_lock_sig_cache.clear(); + self.chain_lock_reversed_sig_cache.clear(); + + // Reset selections and UI state + self.selected_dml_diff_key = None; + self.selected_dml_height_key = None; + self.selected_option_index = None; + self.selected_quorum_in_diff_index = None; + self.selected_masternode_in_diff_index = None; + self.selected_quorum_hash_in_mnlist_diff = None; + self.selected_masternode_pro_tx_hash = None; + self.selected_qr_item = None; + self.selected_core_item = None; + self.pending = None; + self.queued_task = None; + self.search_term = None; + self.error = None; + self.message = None; + } + + /// Clear all data except the oldest MNList diff starting from height 0 + fn clear_keep_base(&mut self) { + let (engine, start_end_diff) = + if let Some(((start, end), oldest_diff)) = self.mnlist_diffs.first_key_value() { + if start == &0 { + MasternodeListEngine::initialize_with_diff_to_height( + oldest_diff.clone(), + *end, + self.app_context.network, + ) + .map(|engine| (engine, Some(((*start, *end), oldest_diff.clone())))) + .unwrap_or(( + MasternodeListEngine::default_for_network(self.app_context.network), + None, + )) + } else { + ( + MasternodeListEngine::default_for_network(self.app_context.network), + None, + ) + } + } else { + ( + MasternodeListEngine::default_for_network(self.app_context.network), + None, + ) + }; + + self.masternode_list_engine = engine; + self.mnlist_diffs = Default::default(); + if let Some((key, oldest_diff)) = start_end_diff { + self.mnlist_diffs.insert(key, oldest_diff); + } + self.selected_dml_diff_key = None; + self.selected_dml_height_key = None; + self.selected_option_index = None; + self.selected_quorum_in_diff_index = None; + self.selected_masternode_in_diff_index = None; + self.selected_quorum_hash_in_mnlist_diff = None; + self.selected_masternode_pro_tx_hash = None; + self.qr_infos = Default::default(); + self.message = None; + // Clear chain lock signatures caches as these are independent of the retained base diff + self.chain_lock_sig_cache.clear(); + self.chain_lock_reversed_sig_cache.clear(); + } + + /// Fetch the MNList diffs between the given base and end block heights. + /// In a real implementation, you would replace the dummy function below with a call to + /// dash_core’s DB (or other data source) to retrieve the MNList diffs. + #[allow(dead_code)] + fn fetch_end_dml_diff(&mut self, validate_quorums: bool) { + let ((base_block_height, base_block_hash), (block_height, block_hash)) = + match self.parse_heights() { + Ok(a) => a, + Err(e) => { + self.error = Some(e); + return; + } + }; + + let mut p2p_handler = match CoreP2PHandler::new(self.app_context.network, None) { + Ok(p2p_handler) => p2p_handler, + Err(e) => { + self.error = Some(e); + return; + } + }; + + self.fetch_single_dml( + &mut p2p_handler, + base_block_hash, + base_block_height, + block_hash, + block_height, + validate_quorums, + ); + + // Reset selections when new data is loaded + self.selected_dml_diff_key = None; + self.selected_quorum_in_diff_index = None; + } + + #[allow(dead_code)] + fn fetch_end_qr_info(&mut self) { + let ((_, base_block_hash), (_, block_hash)) = match self.parse_heights() { + Ok(a) => a, + Err(e) => { + self.error = Some(e); + return; + } + }; + + let mut p2p_handler = match CoreP2PHandler::new(self.app_context.network, None) { + Ok(p2p_handler) => p2p_handler, + Err(e) => { + self.error = Some(e); + return; + } + }; + + self.fetch_rotated_quorum_info(&mut p2p_handler, base_block_hash, block_hash); + + // Reset selections when new data is loaded + self.selected_dml_diff_key = None; + self.selected_quorum_in_diff_index = None; + } + + #[allow(dead_code)] + fn fetch_chain_locks(&mut self) { + let ((base_block_height, _base_block_hash), (block_height, _block_hash)) = + match self.parse_heights() { + Ok(a) => a, + Err(e) => { + self.error = Some(e); + return; + } + }; + + let max_blocks = 2000; + + let loaded_list_height = match self.app_context.network { + Network::Dash => 2227096, + Network::Testnet => 1296600, + _ => 0, + }; + + let start_height = if base_block_height < loaded_list_height { + block_height - max_blocks + } else { + base_block_height + }; + + let end_height = std::cmp::min(start_height + max_blocks, block_height); + + for i in start_height..end_height { + if let Ok(block_hash) = self.get_block_hash_and_cache(i) { + self.get_chain_lock_sig_and_cache(&block_hash).ok(); + } + } + } + + #[allow(dead_code)] + fn sync(&mut self) { + if !self.syncing { + self.syncing = true; + self.fetch_end_qr_info_with_dmls(); + } + } + + #[allow(dead_code)] + fn fetch_end_qr_info_with_dmls(&mut self) { + let ((_, base_block_hash), (_, block_hash)) = match self.parse_heights() { + Ok(a) => a, + Err(e) => { + self.error = Some(e); + return; + } + }; + + let mut p2p_handler = match CoreP2PHandler::new(self.app_context.network, None) { + Ok(p2p_handler) => p2p_handler, + Err(e) => { + self.error = Some(e); + return; + } + }; + + let Some(qr_info) = + self.fetch_rotated_quorum_info(&mut p2p_handler, base_block_hash, block_hash) + else { + return; + }; + + self.feed_qr_info_and_get_dmls(qr_info, Some(p2p_handler)) + } + + fn feed_qr_info_and_get_dmls( + &mut self, + qr_info: QRInfo, + core_p2phandler: Option, + ) { + let mut p2p_handler = match core_p2phandler { + None => match CoreP2PHandler::new(self.app_context.network, None) { + Ok(p2p_handler) => p2p_handler, + Err(e) => { + self.error = Some(e); + return; + } + }, + Some(core_p2phandler) => core_p2phandler, + }; + + // Extracting immutable references before calling `feed_qr_info` + let get_height_fn = { + let block_height_cache = &self.block_height_cache; + let app_context = &self.app_context; + + move |block_hash: &BlockHash| { + if block_hash.as_byte_array() == &[0; 32] { + return Ok(0); + } + if let Some(height) = block_height_cache.get(block_hash) { + return Ok(*height); + } + match app_context + .core_client + .read() + .unwrap() + .get_block_header_info( + &(BlockHash2::from_byte_array(block_hash.to_byte_array())), + ) { + Ok(block_info) => Ok(block_info.height as CoreBlockHeight), + Err(_) => Err(ClientDataRetrievalError::RequiredBlockNotPresent( + *block_hash, + )), + } + } + }; + + if let Err(e) = + self.masternode_list_engine + .feed_qr_info(qr_info, false, true, Some(get_height_fn)) + { + self.error = Some(e.to_string()); + return; + } + + let hashes = self + .masternode_list_engine + .latest_masternode_list_non_rotating_quorum_hashes( + &[LLMQType::Llmqtype50_60, LLMQType::Llmqtype400_85], + true, + ); + self.fetch_diffs_with_hashes(&mut p2p_handler, hashes); + let hashes = self + .masternode_list_engine + .latest_masternode_list_rotating_quorum_hashes(&[]); + for hash in &hashes { + let height = match self.get_height_and_cache(hash) { + Ok(height) => height, + Err(e) => { + self.error = Some(e.to_string()); + return; + } + }; + self.block_height_cache.insert(*hash, height); + } + + if let Some(latest_masternode_list) = self.masternode_list_engine.latest_masternode_list() + && let Err(e) = self + .masternode_list_engine + .verify_non_rotating_masternode_list_quorums( + latest_masternode_list.known_height, + &[LLMQType::Llmqtype50_60, LLMQType::Llmqtype400_85], + ) + { + self.error = Some(e.to_string()); + } + + // Reset selections when new data is loaded + self.selected_dml_diff_key = None; + self.selected_quorum_in_diff_index = None; + } + + /// Render the input area at the top (base and end block height fields plus Get DMLs button) + fn render_input_area(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + ScrollArea::horizontal() + .id_salt("dml_input_row_scroll") + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Base Block Height:"); + ui.add(TextEdit::singleline(&mut self.base_block_height).desired_width(80.0)); + ui.label("End Block Height:"); + ui.add(TextEdit::singleline(&mut self.end_block_height).desired_width(80.0)); + if ui.button("Get single end DML diff").clicked() + && let Ok(((base_h, base_hash), (h, hash))) = self.parse_heights() + { + self.pending = Some(PendingTask::DmlDiffSingle); + action = AppAction::BackendTask(BackendTask::MnListTask( + MnListTask::FetchEndDmlDiff { + base_block_height: base_h, + base_block_hash: base_hash, + block_height: h, + block_hash: hash, + validate_quorums: false, + }, + )); + } + if ui.button("Get single end QR info").clicked() + && let Ok(((_, base_hash), (_, hash))) = self.parse_heights() + { + self.pending = Some(PendingTask::QrInfo); + // Build known_block_hashes from current diffs + base hash (old UI behavior) + let mut known_block_hashes: Vec<_> = self + .mnlist_diffs + .values() + .map(|mn_list_diff| mn_list_diff.block_hash) + .collect(); + known_block_hashes.push(base_hash); + action = AppAction::BackendTask(BackendTask::MnListTask( + MnListTask::FetchEndQrInfo { + known_block_hashes, + block_hash: hash, + }, + )); + } + if ui.button("Get DMLs w/o rotation").clicked() + && let Ok(((base_h, base_hash), (h, hash))) = self.parse_heights() + { + self.pending = Some(PendingTask::DmlDiffNoRotation); + action = AppAction::BackendTask(BackendTask::MnListTask( + MnListTask::FetchEndDmlDiff { + base_block_height: base_h, + base_block_hash: base_hash, + block_height: h, + block_hash: hash, + validate_quorums: true, + }, + )); + } + if ui.button("Get DMLs w/ rotation").clicked() + && let Ok(((_, base_hash), (_, hash))) = self.parse_heights() + { + self.pending = Some(PendingTask::QrInfoWithDmls); + // Build known_block_hashes from current diffs + base hash (old UI behavior) + let mut known_block_hashes: Vec<_> = self + .mnlist_diffs + .values() + .map(|mn_list_diff| mn_list_diff.block_hash) + .collect(); + known_block_hashes.push(base_hash); + action = AppAction::BackendTask(BackendTask::MnListTask( + MnListTask::FetchEndQrInfoWithDmls { + known_block_hashes, + block_hash: hash, + }, + )); + } + if ui.button("Sync").clicked() + && let Ok(((_, base_hash), (_, hash))) = self.parse_heights() + { + self.pending = Some(PendingTask::QrInfoWithDmls); + // Build known_block_hashes from current diffs + base hash (old UI behavior) + let mut known_block_hashes: Vec<_> = self + .mnlist_diffs + .values() + .map(|mn_list_diff| mn_list_diff.block_hash) + .collect(); + known_block_hashes.push(base_hash); + action = AppAction::BackendTask(BackendTask::MnListTask( + MnListTask::FetchEndQrInfoWithDmls { + known_block_hashes, + block_hash: hash, + }, + )); + } + if ui.button("Get chain locks").clicked() + && let Ok(((base_h, _), (h, _))) = self.parse_heights() + { + self.pending = Some(PendingTask::ChainLocks); + action = AppAction::BackendTask(BackendTask::MnListTask( + MnListTask::FetchChainLocks { + base_block_height: base_h, + block_height: h, + }, + )); + } + if ui + .button("Clear") + .on_hover_text("Clear all data and reset to initial state.") + .clicked() + { + self.clear(); + self.display_message("Cleared all data", MessageType::Success); + } + if ui + .button("Clear keep base") + .on_hover_text( + "Clear all data except the oldest MNList diff starting from height 0.", + ) + .clicked() + { + self.clear_keep_base(); + self.display_message( + "Cleared data and kept base diff", + MessageType::Success, + ); + } + }); + // Add bottom padding so the horizontal scrollbar doesn't overlap buttons + ui.add_space(12.0); + }); + action + } + + fn load_masternode_list_engine(&mut self) { + if let Some(path) = rfd::FileDialog::new() + .add_filter("Binary", &["dat"]) + .pick_file() + { + match std::fs::read(&path) { + Ok(bytes) => { + match bincode::decode_from_slice::( + &bytes, + bincode::config::standard(), + ) { + Ok((engine, _)) => { + self.masternode_list_engine = engine; + } + Err(e) => { + eprintln!("Failed to decode QRInfo: {}", e); + } + } + } + Err(e) => { + eprintln!("Failed to read file: {:?}", e); + } + } + } + } + + fn save_masternode_list_engine(&mut self) { + // Serialize the masternode list engine + let serialized = match self.serialize_masternode_list_engine() { + Ok(serialized) => serialized, + Err(e) => { + self.error = Some(format!("Serialization failed: {}", e)); + return; + } + }; + + // Open a file save dialog + if let Some(path) = FileDialog::new() + .set_title("Save Masternode List Engine") + .add_filter("JSON", &["hex"]) + .add_filter("Binary", &["bin"]) + .set_file_name("masternode_list_engine.hex") + .save_file() + { + // Attempt to write the serialized data to the selected file + match fs::write(&path, serialized) { + Ok(_) => { + println!("Masternode list engine saved to {:?}", path); + } + Err(e) => { + self.error = Some(format!("Failed to save file: {}", e)); + } + } + } + } + + fn render_masternode_lists(&mut self, ui: &mut Ui) { + ui.heading("Masternode lists"); + ScrollArea::vertical() + .id_salt("dml_list_scroll_area") + .show(ui, |ui| { + for height in self.masternode_list_engine.masternode_lists.keys() { + let height_label = format!("{}", height); + + if ui + .selectable_label( + self.selected_dml_height_key == Some(*height), + height_label, + ) + .clicked() + { + self.selected_dml_diff_key = None; + self.selected_dml_height_key = Some(*height); + self.selected_quorum_in_diff_index = None; + } + } + }); + } + + /// Render MNList diffs list (block heights) + fn render_diff_list(&mut self, ui: &mut Ui) { + ui.heading("MNList Diffs"); + ScrollArea::vertical() + .id_salt("dml_list_scroll_area") + .show(ui, |ui| { + for (key, _dml) in self.mnlist_diffs.iter() { + let block_label = format!("Base: {} -> Block: {}", key.0, key.1); + + if ui + .selectable_label(self.selected_dml_diff_key == Some(*key), block_label) + .clicked() + { + self.selected_dml_diff_key = Some(*key); + self.selected_dml_height_key = None; + self.selected_quorum_in_diff_index = None; + } + } + }); + } + + /// Render the list of quorums for the selected DML + fn render_new_quorums(&mut self, ui: &mut Ui) { + ui.heading("New Quorums"); + + let should_get_heights = if let Some(selected_key) = self.selected_dml_diff_key { + if self.mnlist_diffs.contains_key(&selected_key) { + !self + .dml_diffs_with_cached_quorum_heights + .contains(&selected_key) + } else { + false + } + } else { + false + }; + + let heights = if should_get_heights { + if let Some(selected_key) = self.selected_dml_diff_key { + if let Some(quorums) = self + .mnlist_diffs + .get(&selected_key) + .map(|dml| dml.new_quorums.clone()) + { + let mut map = HashMap::new(); + for quorum in quorums { + let height = self + .get_height_and_cache(&quorum.quorum_hash) + .ok() + .unwrap_or_default(); + map.insert(quorum.quorum_hash, height); + } + map + } else { + HashMap::new() + } + } else { + HashMap::new() + } + } else if let Some(selected_key) = self.selected_dml_diff_key { + if let Some(quorums) = self + .mnlist_diffs + .get(&selected_key) + .map(|dml| dml.new_quorums.clone()) + { + let mut map = HashMap::new(); + for quorum in quorums { + let height = self + .get_height(&quorum.quorum_hash) + .ok() + .unwrap_or_default(); + map.insert(quorum.quorum_hash, height); + } + map + } else { + HashMap::new() + } + } else { + HashMap::new() + }; + + let new_quorums = self + .selected_dml_diff_key + .and_then(|selected_key| self.mnlist_diffs.get(&selected_key)) + .map(|diff| &diff.new_quorums); + + if let Some(new_quorums) = new_quorums { + ScrollArea::vertical() + .id_salt("quorum_list_scroll_area") + .show(ui, |ui| { + for (q_index, quorum) in new_quorums.iter().enumerate() { + let quorum_height = heights + .get(&quorum.quorum_hash) + .copied() + .unwrap_or_default(); + if ui + .selectable_label( + self.selected_quorum_in_diff_index == Some(q_index), + format!( + "Quorum height {} [..]{}{} Type: {}", + quorum_height, + quorum.quorum_hash.to_string().as_str().split_at(58).1, + quorum + .quorum_index + .map(|i| format!(" (index {})", i)) + .unwrap_or_default(), + QuorumType::from(quorum.llmq_type as u32) + ), + ) + .clicked() + { + self.selected_quorum_in_diff_index = Some(q_index); + self.selected_masternode_in_diff_index = None; + } + } + }); + } else { + ui.label("Select a block height to show quorums."); + } + } + + fn render_selected_masternode_list_items(&mut self, ui: &mut Ui) { + ui.heading("Masternode List Explorer"); + + // Define available options for selection + let options = ["Quorums", "Masternodes"]; + let selected_index = self.selected_option_index.unwrap_or(0); + + // Render the selection buttons + ui.horizontal(|ui| { + for (index, option) in options.iter().enumerate() { + if ui + .selectable_label(selected_index == index, *option) + .clicked() + { + self.selected_option_index = Some(index); + } + } + }); + + ui.separator(); + + // Borrow mn_list separately to avoid multiple borrows of `self` + if self.selected_dml_height_key.is_some() { + ScrollArea::vertical() + .id_salt("mnlist_items_scroll_area") + .show(ui, |ui| match selected_index { + 0 => self.render_quorums_in_masternode_list(ui), + 1 => self.render_masternodes_in_masternode_list(ui), + _ => (), + }); + } else { + ui.label("Select a block height to show details."); + } + } + + fn render_quorums_in_masternode_list(&mut self, ui: &mut Ui) { + let mut heights: BTreeMap = BTreeMap::new(); + let mut masternode_block_hash = None; + if let Some(selected_height) = self.selected_dml_height_key { + if !self + .masternode_lists_with_all_quorum_heights_known + .contains(&selected_height) + { + if let Some(quorum_hashes) = self + .masternode_list_engine + .masternode_lists + .get(&selected_height) + .map(|list| { + list.quorums + .values() + .flat_map(|quorums| quorums.keys()) + .copied() + .collect::>() + }) + { + for quorum_hash in quorum_hashes.iter() { + if let Ok(height) = self.get_height_and_cache(quorum_hash) { + heights.insert(*quorum_hash, height); + } + } + } + self.masternode_lists_with_all_quorum_heights_known + .insert(selected_height); + } + if let Some(mn_list) = self + .masternode_list_engine + .masternode_lists + .get(&selected_height) + { + masternode_block_hash = Some(mn_list.block_hash); + for (llmq_type, quorum_map) in &mn_list.quorums { + if llmq_type == &LLMQType::Llmqtype50_60 + || llmq_type == &LLMQType::Llmqtype400_85 + { + continue; + } + for quorum_hash in quorum_map.keys() { + if let Ok(height) = self.get_height(quorum_hash) { + heights.insert(*quorum_hash, height); + } + } + } + self.masternode_list_quorum_hash_cache + .entry(mn_list.block_hash) + .or_insert_with(|| { + let mut btree_map = BTreeMap::new(); + for (llmq_type, quorum_map) in &mn_list.quorums { + let quorums_by_height = quorum_map + .iter() + .map(|(quorum_hash, quorum_entry)| { + ( + heights.get(quorum_hash).copied().unwrap_or_default(), + quorum_entry.clone(), + ) + }) + .collect(); + btree_map.insert(*llmq_type, quorums_by_height); + } + btree_map + }); + } + } + if let Some(quorums) = masternode_block_hash + .and_then(|block_hash| self.masternode_list_quorum_hash_cache.get(&block_hash)) + { + ui.heading("Quorums in Masternode List"); + ui.label("(excluding 50_60 and 400_85)"); + ScrollArea::vertical() + .id_salt("quorum_list_scroll_area") + .show(ui, |ui| { + for (llmq_type, quorum_map) in quorums { + if llmq_type == &LLMQType::Llmqtype50_60 + || llmq_type == &LLMQType::Llmqtype400_85 + { + continue; + } + for (quorum_height, quorum_entry) in quorum_map.iter() { + if ui + .selectable_label( + self.selected_quorum_hash_in_mnlist_diff + == Some(( + *llmq_type, + quorum_entry.quorum_entry.quorum_hash, + )), + format!( + "Quorum {} Type: {} Valid {}", + quorum_height, + QuorumType::from(*llmq_type as u32), + quorum_entry.verified + == LLMQEntryVerificationStatus::Verified + ), + ) + .clicked() + { + self.selected_quorum_hash_in_mnlist_diff = + Some((*llmq_type, quorum_entry.quorum_entry.quorum_hash)); + self.selected_masternode_pro_tx_hash = None; + self.selected_dml_diff_key = None; + } + } + } + }); + } + } + + /// Filter masternodes based on the search term + fn filter_masternodes( + &self, + mn_list: &MasternodeList, + ) -> BTreeMap { + // If no search term, return all masternodes + if let Some(search_term) = &self.search_term { + let search_term = search_term.to_lowercase(); + + if search_term.len() < 3 { + return mn_list.masternodes.clone(); // Require at least 3 characters to filter + } + + mn_list + .masternodes + .iter() + .filter(|(pro_tx_hash, mn_entry)| { + let masternode = &mn_entry.masternode_list_entry; + + // Convert fields to lowercase for case-insensitive search + let pro_tx_hash_str = pro_tx_hash.to_string().to_lowercase(); + let confirmed_hash_str = masternode + .confirmed_hash + .map(|h| h.to_string().to_lowercase()) + .unwrap_or_default(); + let service_ip = masternode.service_address.ip().to_string().to_lowercase(); + let operator_public_key = + masternode.operator_public_key.to_string().to_lowercase(); + let voting_key_id = masternode.key_id_voting.to_string().to_lowercase(); + + // Check reversed versions + let pro_tx_hash_reversed = pro_tx_hash.reverse().to_string().to_lowercase(); + let confirmed_hash_reversed = masternode + .confirmed_hash + .map(|h| h.reverse().to_string().to_lowercase()) + .unwrap_or_default(); + + // Match against search term + pro_tx_hash_str.contains(&search_term) + || confirmed_hash_str.contains(&search_term) + || service_ip.contains(&search_term) + || operator_public_key.contains(&search_term) + || voting_key_id.contains(&search_term) + || pro_tx_hash_reversed.contains(&search_term) + || confirmed_hash_reversed.contains(&search_term) + }) + .map(|(pro_tx_hash, entry)| (*pro_tx_hash, entry.clone())) + .collect() + } else { + mn_list.masternodes.clone() + } + } + + /// Render search bar + fn render_search_bar(&mut self, ui: &mut Ui) { + ui.horizontal(|ui| { + ui.label("Search:"); + let mut search_term = self.search_term.clone().unwrap_or_default(); + let response = ui.add(TextEdit::singleline(&mut search_term).desired_width(200.0)); + + if response.changed() { + self.search_term = if search_term.trim().is_empty() { + None + } else { + Some(search_term) + }; + } + }); + } + + fn render_masternodes_in_masternode_list(&mut self, ui: &mut Ui) { + if let Some(selected_height) = self.selected_dml_height_key + && self + .masternode_list_engine + .masternode_lists + .contains_key(&selected_height) + { + ui.heading("Masternodes in List"); + self.render_search_bar(ui); + } + if let Some(selected_height) = self.selected_dml_height_key + && let Some(mn_list) = self + .masternode_list_engine + .masternode_lists + .get(&selected_height) + { + let filtered_masternodes = self.filter_masternodes(mn_list); + ScrollArea::vertical() + .id_salt("masternode_list_scroll_area") + .show(ui, |ui| { + for (pro_tx_hash, masternode) in filtered_masternodes.iter() { + if ui + .selectable_label( + self.selected_masternode_pro_tx_hash == Some(*pro_tx_hash), + format!( + "{} {} {}", + if masternode.masternode_list_entry.mn_type + == EntryMasternodeType::Regular + { + "MN" + } else { + "EN" + }, + masternode.masternode_list_entry.service_address.ip(), + pro_tx_hash.to_string().as_str().split_at(5).0 + ), + ) + .clicked() + { + self.selected_quorum_hash_in_mnlist_diff = None; + self.selected_masternode_pro_tx_hash = Some(*pro_tx_hash); + } + } + }); + } + } + + fn render_masternode_list_page(&mut self, ui: &mut Ui) { + // Use a left-to-right layout that fills the available height so columns can expand fully + let full_w = ui.available_width(); + let full_h = ui.available_height(); + ui.allocate_ui_with_layout( + egui::Vec2::new(full_w, full_h), + Layout::left_to_right(Align::Min), + |ui| { + // Left column (Fixed width: 120px) + ui.allocate_ui_with_layout( + egui::Vec2::new(120.0, ui.available_height()), + Layout::top_down(Align::Min), + |ui| { + self.render_masternode_lists(ui); + }, + ); + + ui.separator(); + + // Middle column (40% of the remaining space) + let mid_w = ui.available_width() * 0.4; + ui.allocate_ui_with_layout( + egui::Vec2::new(mid_w, ui.available_height()), + Layout::top_down(Align::Min), + |ui| { + self.render_selected_masternode_list_items(ui); + }, + ); + + // Right column (Remaining space) + ui.allocate_ui_with_layout( + egui::Vec2::new(ui.available_width(), ui.available_height()), + Layout::top_down(Align::Min), + |ui| { + if self.selected_quorum_hash_in_mnlist_diff.is_some() { + self.render_quorum_details(ui); + } else if self.selected_masternode_pro_tx_hash.is_some() { + self.render_mn_details(ui); + } + }, + ); + }, + ); + } + + fn render_selected_tab(&mut self, ui: &mut Ui) { + // Define available tabs + let mut tabs = vec![ + "Masternode Lists", + "Quorums", + "Diffs", + "QRInfo", + "Known Blocks", + "Known Chain Lock Sigs", + "Core Items", + "Save Masternode List Engine", + "Load Masternode List Engine", + ]; + + if self.syncing { + tabs.push("Stop Syncing"); + } + + // Render the selection buttons (scrollable horizontally) styled as buttons + ScrollArea::horizontal() + .id_salt("dml_tabs_scroll") + .show(ui, |ui| { + ui.horizontal(|ui| { + for (index, tab) in tabs.iter().enumerate() { + let is_selected = self.selected_tab == index; + if is_selected { + // Match the selected look used under "Masternode List Explorer" + let _ = ui.selectable_label(true, *tab); + } else if ui.button(*tab).clicked() { + match index { + 7 => { + // Show the popup when "Masternode List Engine" is selected + self.show_popup_for_render_masternode_list_engine = true; + } + 8 => { + self.load_masternode_list_engine(); + } + 9 => { + self.syncing = false; + } + index => self.selected_tab = index, + } + } + } + }); + // Add bottom padding so the horizontal scrollbar doesn't overlap tabs + ui.add_space(12.0); + }); + + ui.separator(); + + // Scroll only the content below the tab row; for the Masternode Lists page, + // let its own columns manage scrolling independently. + if self.selected_tab == 0 { + // Make the Masternode Lists section occupy remaining height + let full_w = ui.available_width(); + let full_h = ui.available_height(); + ui.allocate_ui_with_layout( + egui::Vec2::new(full_w, full_h), + Layout::top_down(Align::Min), + |ui| { + self.render_masternode_list_page(ui); + }, + ); + } else { + ScrollArea::vertical() + .auto_shrink([false; 2]) + .id_salt("dml_tab_content_scroll") + .show(ui, |ui| match self.selected_tab { + 1 => self.render_quorums(ui), + 2 => self.render_diffs(ui), + 3 => self.render_qr_info(ui), + 4 => self.render_engine_known_blocks(ui), + 5 => self.render_known_chain_lock_sigs(ui), + 6 => self.render_core_items(ui), + _ => {} + }); + } + + // Render the confirmation popup if needed + if self.show_popup_for_render_masternode_list_engine { + egui::Window::new("Confirmation") + .collapsible(false) + .resizable(false) + .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) + .show(ui.ctx(), |ui| { + ui.label("This operation will take about 10 seconds. Are you sure you wish to continue?"); + + ui.horizontal(|ui| { + if ui.button("Yes").clicked() { + self.save_masternode_list_engine(); + self.show_popup_for_render_masternode_list_engine = false; + } + if ui.button("Cancel").clicked() { + self.show_popup_for_render_masternode_list_engine = false; + } + }); + }); + } + } + + fn render_known_chain_lock_sigs(&mut self, ui: &mut Ui) { + ui.heading("Known Chain Lock Sigs"); + + ScrollArea::vertical() + .id_salt("known_chain_lock_sigs_scroll") + .show(ui, |ui| { + egui::Grid::new("known_chain_lock_sigs_grid") + .num_columns(3) // Two columns: Block Height | Block Hash | Sig + .striped(true) + .show(ui, |ui| { + ui.label("Block Height"); + ui.label("Block Hash"); + ui.label("Chain Lock Sig"); + ui.end_row(); + + for ((height, block_hash), sig) in &self.chain_lock_sig_cache { + ui.label(format!("{}", height)); + ui.label(format!("{}", block_hash)); + if let Some(sig) = sig { + ui.label(format!("{}", sig)); + } else { + ui.label("None"); + } + + ui.end_row(); + } + }); + }); + } + + fn render_engine_known_blocks(&mut self, ui: &mut Ui) { + ui.heading("Known Blocks in Masternode List Engine"); + + // Add Save/Load functionality + ui.horizontal(|ui| { + if ui.button("Save Block Container").clicked() { + // Open native save dialog + if let Some(path) = FileDialog::new() + .set_file_name("block_container.dat") + .add_filter("Data Files", &["dat"]) + .save_file() + { + // Serialize and save the block container + let serialized_data = bincode::encode_to_vec( + &self.masternode_list_engine.block_container, + bincode::config::standard(), + ) + .expect("serialize container"); + if let Err(e) = std::fs::write(&path, serialized_data) { + eprintln!("Failed to write file: {}", e); + } + } + } + }); + + ScrollArea::vertical() + .id_salt("known_blocks_scroll") + .show(ui, |ui| { + ui.label(format!( + "Total Known Blocks: {}", + self.masternode_list_engine + .block_container + .known_block_count() + )); + + egui::Grid::new("known_blocks_grid") + .num_columns(2) // Two columns: Block Height | Block Hash + .striped(true) + .show(ui, |ui| { + ui.label("Block Height"); + ui.label("Block Hash"); + ui.end_row(); + + let MasternodeListEngineBlockContainer::BTreeMapContainer(map) = + &self.masternode_list_engine.block_container; + + // Sort block heights for ordered display + let mut known_blocks: Vec<_> = map.block_heights.iter().collect(); + known_blocks.sort_by_key(|(_, height)| *height); + + for (block_hash, height) in known_blocks { + ui.label(format!("{}", height)); + let hash_str = format!("{}", block_hash); + + if ui.selectable_label(false, hash_str.clone()).clicked() { + ui.ctx().copy_text(hash_str.clone()); + } + + ui.end_row(); + } + }); + }); + } + + fn render_diffs(&mut self, ui: &mut Ui) { + // Add Save/Load functionality + ui.horizontal(|ui| { + if ui.button("Save MN List Diffs").clicked() { + // Open native save dialog + if let Some(path) = FileDialog::new() + .set_file_name("mnlistdiffs.dat") + .add_filter("Data Files", &["dat"]) + .save_file() + { + // Serialize and save the block container + let serialized_data = + bincode::encode_to_vec(&self.mnlist_diffs, bincode::config::standard()) + .expect("serialize container"); + if let Err(e) = std::fs::write(&path, serialized_data) { + eprintln!("Failed to write file: {}", e); + } + } + } + }); + // Create a three-column layout: + // - Left column: list of MNList Diffs (by block height) + // - Middle column: list of quorums for the selected DML + // - Right column: quorum details + ui.horizontal(|ui| { + ui.allocate_ui_with_layout( + egui::Vec2::new(150.0, 800.0), // Set fixed width for left column + Layout::top_down(Align::Min), + |ui| { + self.render_diff_list(ui); + }, + ); + + ui.separator(); // Optional: Adds a visual separator + + ui.allocate_ui_with_layout( + egui::Vec2::new(ui.available_width() * 0.4, 800.0), // Middle column + Layout::top_down(Align::Min), + |ui| { + self.render_selected_dml_items(ui); + }, + ); + + ui.allocate_ui_with_layout( + egui::Vec2::new(ui.available_width(), ui.available_height()), // Right column takes remaining space + Layout::top_down(Align::Min), + |ui| { + if self.selected_quorum_in_diff_index.is_some() { + self.render_quorum_details(ui); + } else if self.selected_masternode_in_diff_index.is_some() { + self.render_mn_details(ui); + } + }, + ); + }); + } + + fn render_masternode_changes(&mut self, ui: &mut Ui) { + ui.heading("Masternode changes"); + if let Some(selected_key) = self.selected_dml_diff_key { + if let Some(dml) = self.mnlist_diffs.get(&selected_key) { + ScrollArea::vertical() + .id_salt("quorum_list_scroll_area") + .show(ui, |ui| { + for (m_index, masternode) in dml.new_masternodes.iter().enumerate() { + if ui + .selectable_label( + self.selected_masternode_in_diff_index == Some(m_index), + format!( + "{} {} {}", + if masternode.mn_type == EntryMasternodeType::Regular { + "MN" + } else { + "EN" + }, + masternode.service_address.ip(), + masternode + .pro_reg_tx_hash + .to_string() + .as_str() + .split_at(5) + .0 + ), + ) + .clicked() + { + self.selected_quorum_in_diff_index = None; + self.selected_masternode_in_diff_index = Some(m_index); + } + } + }); + } + } else { + ui.label("Select a block height to show quorums."); + } + } + + fn render_mn_diff_chain_locks(&mut self, ui: &mut Ui) { + ui.heading("MN list diff chain locks"); + if let Some(selected_key) = self.selected_dml_diff_key + && let Some(dml) = self.mnlist_diffs.get(&selected_key) + { + ScrollArea::vertical() + .id_salt("quorum_list_chain_locks_scroll_area") + .show(ui, |ui| { + for (index, sig) in dml.quorums_chainlock_signatures.iter().enumerate() { + ui.group(|ui| { + ui.label(format!("Signature #{}", index)); + ui.monospace(format!( + "Signature: {}", + hex::encode(sig.signature.as_bytes()) + )); + ui.label(format!("Index Set: {:?}", sig.index_set)); + }); + } + }); + } + } + + fn save_mn_list_diff(&mut self) { + let Some(selected_key) = self.selected_dml_diff_key else { + self.error = Some("No MNListDiff selected.".to_string()); + return; + }; + + let Some(mn_list_diff) = self.mnlist_diffs.get(&selected_key) else { + self.error = Some("Failed to retrieve selected MNListDiff.".to_string()); + return; + }; + + // Extract block heights from the selected key + let (base_block_height, block_height) = selected_key; + + // Serialize the MNListDiff + let serialized = serialize(mn_list_diff); + + // Generate the dynamic filename + let file_name = format!("mn_list_diff_{}_{}.bin", base_block_height, block_height); + + // Open a file save dialog with the generated file name + if let Some(path) = FileDialog::new() + .set_title("Save MNListDiff") + .add_filter("Binary", &["bin"]) + .set_file_name(&file_name) // Set the dynamic filename + .save_file() + { + // Attempt to write the serialized data to the selected file + match fs::write(&path, serialized) { + Ok(_) => { + println!("MNListDiff saved to {:?}", path); + } + Err(e) => { + self.error = Some(format!("Failed to save file: {}", e)); + } + } + } + } + + /// Render the list of items for the selected DML, with a selector at the top + fn render_selected_dml_items(&mut self, ui: &mut Ui) { + ui.heading("Masternode List Diff Explorer"); + + // Define available options for selection + let options = [ + "New Quorums", + "Masternode Changes", + "Chain Locks", + "Save Diff", + ]; + let selected_index = self.selected_option_index.unwrap_or(0); + + // Render the selection buttons + ui.horizontal(|ui| { + for (index, option) in options.iter().enumerate() { + if ui + .selectable_label(selected_index == index, *option) + .clicked() + { + // If the user selects "Save MNListDiff", trigger save function + if index == 3 { + self.save_mn_list_diff(); + } else { + self.selected_option_index = Some(index); + } + } + } + }); + + ui.separator(); + + // Determine the selected category and display corresponding information + if let Some(selected_key) = self.selected_dml_diff_key { + if self.mnlist_diffs.contains_key(&selected_key) { + ScrollArea::vertical() + .id_salt("dml_items_scroll_area") + .show(ui, |ui| match selected_index { + 0 => self.render_new_quorums(ui), + 1 => self.render_masternode_changes(ui), + 2 => self.render_mn_diff_chain_locks(ui), + _ => (), + }); + } + } else { + ui.label("Select a block height to show details."); + } + } + + pub fn required_cl_sig_heights(&self, quorum: &QuorumEntry) -> BTreeSet { + let mut required_heights = BTreeSet::new(); + let Ok(quorum_block_height) = self.get_height(&quorum.quorum_hash) else { + return BTreeSet::new(); + }; + let llmq_params = quorum.llmq_type.params(); + let quorum_index = quorum_block_height % llmq_params.dkg_params.interval; + let cycle_base_height = quorum_block_height - quorum_index; + let cycle_length = llmq_params.dkg_params.interval; + for i in 0..=3 { + required_heights.insert(cycle_base_height - i * cycle_length - 8); + } + required_heights + } + + /// Render the details for the selected quorum + fn render_quorum_details(&mut self, ui: &mut Ui) { + ui.heading("Quorum Details"); + if let Some(dml_key) = self.selected_dml_diff_key { + if let Some(dml) = self.mnlist_diffs.get(&dml_key) { + if let Some(q_index) = self.selected_quorum_in_diff_index { + if let Some(quorum) = dml.new_quorums.get(q_index) { + Frame::NONE + .stroke(Stroke::new(1.0, Color32::BLACK)) + .show(ui, |ui| { + ui.set_min_size(Vec2::new(ui.available_width(), 300.0)); + let height = self.get_height(&quorum.quorum_hash).ok(); + + // Build a vector of optional signatures with slots matching new_quorums length + let mut quorum_sig_lookup: Vec> = vec![None; dml.new_quorums.len()]; + + // Fill each slot with the corresponding signature + for quorum_sig_obj in &dml.quorums_chainlock_signatures { + for &index in &quorum_sig_obj.index_set { + if let Some(slot) = quorum_sig_lookup.get_mut(index as usize) { + *slot = Some(&quorum_sig_obj.signature); + } else { + return; + } + } + } + + // Verify all slots have been filled + if quorum_sig_lookup.iter().any(Option::is_none) { + return; + } + + let chain_lock_msg = if let Some(a) = quorum_sig_lookup.get(q_index) { + if let Some(b) = a { + hex::encode(b) + } else { + "Error a".to_string() + } + } else { + "Error b".to_string() + }; + + let expected_chain_lock_sig = if let Some(height) = height { + if let Ok(hash) = self.get_block_hash(height - 8) { + if let Ok(Some(sig)) = self.get_chain_lock_sig(&hash) { + hex::encode(sig) + } else { + "Error (Did not find chain lock sig for hash)".to_string() + } + } else { + "Error (Did not find block hash of 8 blocks ago)".to_string() + } + } else { + "Error (Did not find quorum hash height)".to_string() + }; + if quorum.llmq_type.is_rotating_quorum_type() { + ScrollArea::vertical().id_salt("render_quorum_details").show(ui, |ui| { + ui.label(format!( + "Version: {}\nQuorum Hash Height: {}\nQuorum Hash: {}\nCycle Hash Height: {}\nQuorum Index: {}\nSigners: {} members\nValid Members: {} members\nQuorum Public Key: {}\nAssociated Chain Lock Sig: {}\nExpected Chain Lock Sig: {}", + quorum.version, + self.get_height(&quorum.quorum_hash).ok().map(|height| format!("{}", height)).unwrap_or("Unknown".to_string()), + quorum.quorum_hash, + self.get_height(&quorum.quorum_hash).ok().and_then(|height| quorum.quorum_index.map(|index| format!("{}", height - index as CoreBlockHeight))).unwrap_or("Unknown".to_string()), + quorum.quorum_index.map(|quorum_index| quorum_index.to_string()).unwrap_or("Unknown".to_string()), + quorum.signers.iter().filter(|&&b| b).count(), + quorum.valid_members.iter().filter(|&&b| b).count(), + quorum.quorum_public_key, + chain_lock_msg, + expected_chain_lock_sig, + )); + }); + } else { + ScrollArea::vertical().id_salt("render_quorum_details").show(ui, |ui| { + ui.label(format!( + "Version: {}\nQuorum Hash Height: {}\nQuorum Hash: {}\nSigners: {} members\nValid Members: {} members\nQuorum Public Key: {}\nAssociated Chain Lock Sig: {}\nExpected Chain Lock Sig: {}", + quorum.version, + self.get_height(&quorum.quorum_hash).ok().map(|height| format!("{}", height)).unwrap_or("Unknown".to_string()), + quorum.quorum_hash, + quorum.signers.iter().filter(|&&b| b).count(), + quorum.valid_members.iter().filter(|&&b| b).count(), + quorum.quorum_public_key, + chain_lock_msg, + expected_chain_lock_sig, + )); + }); + } + }); + } + } else { + ui.label("Select a quorum to view details."); + } + } + } else if let Some(selected_height) = self.selected_dml_height_key { + if let Some(mn_list) = self + .masternode_list_engine + .masternode_lists + .get(&selected_height) + { + if let Some((llmq_type, quorum_hash)) = self.selected_quorum_hash_in_mnlist_diff { + if let Some(quorum) = mn_list + .quorums + .get(&llmq_type) + .and_then(|quorums_by_type| quorums_by_type.get(&quorum_hash)) + { + let height = self.get_height(&quorum.quorum_entry.quorum_hash).ok(); + let chain_lock_sig = + if quorum.quorum_entry.llmq_type.is_rotating_quorum_type() { + let heights = self.required_cl_sig_heights(&quorum.quorum_entry); + format!( + "heights [{}]", + heights.iter().map(|h| h.to_string()).join(" | ") + ) + } else if let Some(height) = height { + if let Ok(hash) = self.get_block_hash(height - 8) { + if let Ok(Some(sig)) = self.get_chain_lock_sig(&hash) { + hex::encode(sig) + } else { + "Error (Did not find chain lock sig for hash)".to_string() + } + } else { + "Error (Did not find block hash of 8 blocks ago)".to_string() + } + } else { + "Error (Did not find quorum hash height)".to_string() + }; + + let get_used_heights = |bls_signature: BLSSignature| { + let Some(used) = self.chain_lock_reversed_sig_cache.get(&bls_signature) + else { + return String::default(); + }; + if used.is_empty() { + String::default() + } else if used.len() == 1 { + format!(" [height: {}]", used.iter().next().unwrap().0) + } else { + format!( + " [height: {} to {}]", + used.iter().next().unwrap().0, + used.last().unwrap().0 + ) + } + }; + + let associated_chain_lock_sig = match quorum.verifying_chain_lock_signature + { + Some(VerifyingChainLockSignaturesType::NonRotating( + associated_chain_lock_sig, + )) => hex::encode(associated_chain_lock_sig), + Some(VerifyingChainLockSignaturesType::Rotating( + associated_chain_lock_sigs, + )) => { + format!( + "[\n-3: {}{}\n-2: {}{}\n-1: {}{}\n0: {}{}\n]", + hex::encode(associated_chain_lock_sigs[0]), + get_used_heights(associated_chain_lock_sigs[0]), + hex::encode(associated_chain_lock_sigs[1]), + get_used_heights(associated_chain_lock_sigs[1]), + hex::encode(associated_chain_lock_sigs[2]), + get_used_heights(associated_chain_lock_sigs[2]), + hex::encode(associated_chain_lock_sigs[3]), + get_used_heights(associated_chain_lock_sigs[3]) + ) + } + None => "None set".to_string(), + }; + + Frame::NONE + .stroke(Stroke::new(1.0, Color32::BLACK)) + .show(ui, |ui| { + ui.set_min_size(Vec2::new(ui.available_width(), 300.0)); + ScrollArea::vertical().id_salt("render_quorum_details_2").show(ui, |ui| { + ui.label(format!( + "Quorum Type: {}\nQuorum Height: {}\nQuorum Hash: {}\nCommitment Hash: {}\nCommitment Data: {}\nEntry Hash: {}\nSigners: {} members\nValid Members: {} members\nQuorum Public Key: {}\nValidation Status: {}\nAssociated Chain Lock Sig: {}\nExpected Chain Lock Sig: {}", + QuorumType::from(quorum.quorum_entry.llmq_type as u32), + self.get_height(&quorum.quorum_entry.quorum_hash).ok().map(|height| format!("{}", height)).unwrap_or("Unknown".to_string()), + quorum.quorum_entry.quorum_hash, + quorum.commitment_hash, + hex::encode(quorum.quorum_entry.commitment_data()), + quorum.entry_hash, + quorum.quorum_entry.signers.iter().filter(|&&b| b).count(), + quorum.quorum_entry.valid_members.iter().filter(|&&b| b).count(), + quorum.quorum_entry.quorum_public_key, + quorum.verified, + associated_chain_lock_sig, + chain_lock_sig, + )); + }); + }); + } + } else { + ui.label("Select a quorum to view details."); + } + } + } else { + ui.label("Select a block height and quorum."); + } + } + + /// Render the details for the selected Masternode + fn render_mn_details(&mut self, ui: &mut Ui) { + ui.heading("Masternode Details"); + + if let Some(dml_key) = self.selected_dml_diff_key { + if let Some(dml) = self.mnlist_diffs.get(&dml_key) { + if let Some(mn_index) = self.selected_masternode_in_diff_index { + if let Some(masternode) = dml.new_masternodes.get(mn_index) { + Frame::NONE + .stroke(Stroke::new(1.0, Color32::BLACK)) + .show(ui, |ui| { + ui.set_min_size(Vec2::new(ui.available_width(), 300.0)); + ScrollArea::vertical().id_salt("render_mn_details").show( + ui, + |ui| { + ui.label(format!( + "Version: {}\n\ + ProRegTxHash: {}\n\ + Confirmed Hash: {}\n\ + Service Address: {}:{}\n\ + Operator Public Key: {}\n\ + Voting Key ID: {}\n\ + Is Valid: {}\n\ + Masternode Type: {}", + masternode.version, + masternode.pro_reg_tx_hash.reverse(), + match masternode.confirmed_hash { + None => "No confirmed hash".to_string(), + Some(confirmed_hash) => + confirmed_hash.reverse().to_string(), + }, + masternode.service_address.ip(), + masternode.service_address.port(), + masternode.operator_public_key, + masternode.key_id_voting, + masternode.is_valid, + match masternode.mn_type { + EntryMasternodeType::Regular => + "Regular".to_string(), + EntryMasternodeType::HighPerformance { + platform_http_port, + platform_node_id, + } => { + format!( + "High Performance (Port: {}, Node ID: {})", + platform_http_port, platform_node_id + ) + } + } + )); + }, + ); + }); + } + } else { + ui.label("Select a Masternode to view details."); + } + } + } else if let Some(selected_height) = self.selected_dml_height_key { + if let Some(mn_list) = self + .masternode_list_engine + .masternode_lists + .get(&selected_height) + && let Some(selected_pro_tx_hash) = self.selected_masternode_pro_tx_hash + && let Some(qualified_masternode) = mn_list.masternodes.get(&selected_pro_tx_hash) + { + let masternode = &qualified_masternode.masternode_list_entry; + Frame::NONE + .stroke(Stroke::new(1.0, Color32::BLACK)) + .show(ui, |ui| { + ui.set_min_size(Vec2::new(ui.available_width(), 300.0)); + ScrollArea::vertical() + .id_salt("render_mn_details_2") + .show(ui, |ui| { + ui.label(format!( + "Version: {}\n\ + ProRegTxHash: {}\n\ + Confirmed Hash: {}\n\ + Service Address: {}:{}\n\ + Operator Public Key: {}\n\ + Voting Key ID: {}\n\ + Is Valid: {}\n\ + Masternode Type: {}\n\ + Entry Hash: {}\n\ + Confirmed Hash hashed with ProRegTx: {}\n", + masternode.version, + masternode.pro_reg_tx_hash.reverse(), + match masternode.confirmed_hash { + None => "No confirmed hash".to_string(), + Some(confirmed_hash) => + confirmed_hash.reverse().to_string(), + }, + masternode.service_address.ip(), + masternode.service_address.port(), + masternode.operator_public_key, + masternode.key_id_voting, + masternode.is_valid, + match masternode.mn_type { + EntryMasternodeType::Regular => "Regular".to_string(), + EntryMasternodeType::HighPerformance { + platform_http_port, + platform_node_id, + } => { + format!( + "High Performance (Port: {}, Node ID: {})", + platform_http_port, platform_node_id + ) + } + }, + hex::encode(qualified_masternode.entry_hash), + if let Some(hash) = + qualified_masternode.confirmed_hash_hashed_with_pro_reg_tx + { + hash.reverse().to_string() + } else { + "None".to_string() + }, + )); + }); + }); + } + } else { + ui.label("Select a block height and Masternode."); + } + } + + fn render_selected_shapshot_details(ui: &mut Ui, snapshot: &QuorumSnapshot) { + ui.heading("Quorum Snapshot Details"); + + // Display Skip List Mode + ui.label(format!("Skip List Mode: {}", snapshot.skip_list_mode)); + + // Display Active Quorum Members (Bitset) + ui.label(format!( + "Active Quorum Members: {} members", + snapshot.active_quorum_members.len() + )); + + // Show active members in a scrollable area + ScrollArea::vertical() + .id_salt("render_snapshot_details") + .show(ui, |ui| { + ui.label("Active Quorum Members:"); + for (i, active) in snapshot.active_quorum_members.iter().enumerate() { + ui.label(format!( + "Member {}: {}", + i, + if *active { "Active" } else { "Inactive" } + )); + } + }); + + ui.separator(); + + // Display Skip List + ui.label(format!("Skip List: {} entries", snapshot.skip_list.len())); + + // Show skip list entries + ScrollArea::vertical() + .id_salt("render_snapshot_details_2") + .show(ui, |ui| { + ui.label("Skip List Entries:"); + for (i, skip_entry) in snapshot.skip_list.iter().enumerate() { + ui.label(format!("Entry {}: {}", i, skip_entry)); + } + }); + } + + fn render_qr_info(&mut self, ui: &mut Ui) { + ui.heading("QRInfo Viewer"); + + // Select the first available QRInfo if none is selected + let selected_qr_info = { + let Some((_, selected_qr_info)) = self.qr_infos.first_key_value() else { + ui.label("No QRInfo available."); + if ui.button("Load QR Info").clicked() + && let Some(path) = FileDialog::new() + .add_filter("Data Files", &["dat"]) + .pick_file() + { + match std::fs::read(&path) { + Ok(bytes) => { + // Let's first try consensus decode + match QRInfo::consensus_decode(&mut std::io::Cursor::new(&bytes)) { + Ok(qr_info) => { + let key = qr_info.mn_list_diff_tip.block_hash; + self.qr_infos.insert(key, qr_info.clone()); + self.feed_qr_info_and_get_dmls(qr_info, None); + } + Err(_) => { + match bincode::decode_from_slice::( + &bytes, + bincode::config::standard(), + ) { + Ok((qr_info, _)) => { + let key = qr_info.mn_list_diff_tip.block_hash; + self.qr_infos.insert(key, qr_info); + } + Err(e) => { + eprintln!("Failed to decode QRInfo: {}", e); + } + } + } + } + } + Err(e) => { + eprintln!("Failed to read file: {}", e); + } + } + } + return; + }; + selected_qr_info.clone() + }; + + if let Ok(height) = self.get_height(&selected_qr_info.mn_list_diff_tip.block_hash) { + // Add Save/Load functionality + ui.horizontal(|ui| { + if ui.button("Save QR Info").clicked() { + // Open native save dialog + if let Some(path) = FileDialog::new() + .set_file_name(format!("qrinfo_{}.dat", height)) + .add_filter("Data Files", &["dat"]) + .save_file() + { + // Serialize and save the block container + let serialized_data = + bincode::encode_to_vec(&selected_qr_info, bincode::config::standard()) + .expect("serialize container"); + if let Err(e) = std::fs::write(&path, serialized_data) { + eprintln!("Failed to write file: {}", e); + } + } + } + }); + } + + // Track user selections + if self.selected_qr_field.is_none() { + self.selected_qr_field = Some("Quorum Snapshots".to_string()); + } + + ui.horizontal(|ui| { + // Left Panel: Fields of QRInfo + ui.allocate_ui_with_layout( + egui::Vec2::new(180.0, ui.available_height()), + Layout::top_down(Align::Min), + |ui| { + ui.label("QRInfo Fields:"); + let fields = [ + "Rotated Quorums At Index", + "Masternode List Diffs", + "Quorum Snapshots", + "Quorum Snapshot List", + "MN List Diff List", + ]; + + for field in &fields { + if ui + .selectable_label( + self.selected_qr_field.as_deref() == Some(*field), + *field, + ) + .clicked() + { + self.selected_qr_field = Some(field.to_string()); + self.selected_qr_list_index = None; + self.selected_qr_item = None; + } + } + }, + ); + + ui.separator(); + + // Center Panel: Items in the selected field + ui.allocate_ui_with_layout( + egui::Vec2::new(ui.available_width() * 0.5, ui.available_height()), + Layout::top_down(Align::Min), + |ui| { + ui.heading("Selected Field Items"); + + match self.selected_qr_field.as_deref() { + Some("Quorum Snapshots") => { + self.render_quorum_snapshots(ui, &selected_qr_info) + } + Some("Masternode List Diffs") => { + self.render_mn_list_diffs(ui, &selected_qr_info) + } + Some("Rotated Quorums At Index") => self.render_last_commitments( + ui, + selected_qr_info + .last_commitment_per_index + .first() + .map(|entry| entry.quorum_hash), + ), + Some("Quorum Snapshot List") => { + self.render_quorum_snapshot_list(ui, &selected_qr_info) + } + Some("MN List Diff List") => { + self.render_mn_list_diff_list(ui, &selected_qr_info) + } + _ => { + ui.label("Select a field to display."); + } + } + }, + ); + + ui.separator(); + + // Right Panel: Detailed View of Selected Item + ui.allocate_ui_with_layout( + egui::Vec2::new(ui.available_width(), ui.available_height()), + Layout::top_down(Align::Min), + |ui| { + if let Some(selected_item) = &self.selected_qr_item { + match selected_item { + SelectedQRItem::SelectedSnapshot(snapshot) => { + Self::render_selected_shapshot_details(ui, snapshot); + } + SelectedQRItem::MNListDiff(mn_list_diff) => { + self.render_selected_mn_list_diff(ui, mn_list_diff); + } + SelectedQRItem::QuorumEntry(quorum_entry) => { + Self::render_selected_quorum_entry(ui, quorum_entry); + } + } + } else { + ui.label("Select an item to view details."); + } + }, + ); + }); + } + fn render_selected_mn_list_diff(&self, ui: &mut Ui, mn_list_diff: &MnListDiff) { + ui.heading("MNListDiff Details"); + + // General MNListDiff Info + ui.label(format!( + "Version: {}\nBase Block Hash: {} ({})\nBlock Hash: {} ({})", + mn_list_diff.version, + mn_list_diff.base_block_hash, + self.get_height_or_error_as_string(&mn_list_diff.base_block_hash), + mn_list_diff.block_hash, + self.get_height_or_error_as_string(&mn_list_diff.block_hash) + )); + + ui.label(format!( + "Total Transactions: {}", + mn_list_diff.total_transactions + )); + + ui.separator(); + + // Merkle Tree Data + ui.heading("Merkle Tree"); + ui.label(format!( + "Merkle Hashes: {} entries", + mn_list_diff.merkle_hashes.len() + )); + ScrollArea::vertical() + .id_salt("render_selected_mn_list_diff") + .show(ui, |ui| { + for (i, merkle_hash) in mn_list_diff.merkle_hashes.iter().enumerate() { + ui.label(format!("{}: {}", i, merkle_hash)); + } + }); + + ui.separator(); + ui.label(format!( + "Merkle Flags ({} bytes)", + mn_list_diff.merkle_flags.len() + )); + + // Coinbase Transaction + ui.heading("Coinbase Transaction"); + ScrollArea::vertical() + .id_salt("render_selected_mn_list_diff_2") + .show(ui, |ui| { + ui.label(format!( + "Coinbase TXID: {}\nSize: {} bytes", + mn_list_diff.coinbase_tx.txid(), + mn_list_diff.coinbase_tx.size() + )); + }); + + ui.separator(); + + // Masternode Changes + ui.heading("Masternode Changes"); + ui.label(format!( + "New Masternodes: {}\nDeleted Masternodes: {}", + mn_list_diff.new_masternodes.len(), + mn_list_diff.deleted_masternodes.len(), + )); + + ScrollArea::vertical() + .id_salt("render_selected_mn_list_diff_3") + .show(ui, |ui| { + ui.heading("New Masternodes"); + for masternode in &mn_list_diff.new_masternodes { + ui.label(format!( + "{} {}:{}", + masternode.pro_reg_tx_hash, + masternode.service_address.ip(), + masternode.service_address.port(), + )); + } + + ui.separator(); + ui.heading("Removed Masternodes"); + for removed_pro_tx in &mn_list_diff.deleted_masternodes { + ui.label(removed_pro_tx.to_string()); + } + }); + + ui.separator(); + + // Quorum Changes + ui.heading("Quorum Changes"); + ui.label(format!( + "New Quorums: {}\nDeleted Quorums: {}", + mn_list_diff.new_quorums.len(), + mn_list_diff.deleted_quorums.len() + )); + + ScrollArea::vertical() + .id_salt("render_selected_mn_list_diff_4") + .show(ui, |ui| { + ui.heading("New Quorums"); + for quorum in &mn_list_diff.new_quorums { + ui.label(format!( + "Quorum {} Type: {}", + quorum.quorum_hash, + QuorumType::from(quorum.llmq_type as u32) + )); + } + + ui.separator(); + ui.heading("Removed Quorums"); + for deleted_quorum in &mn_list_diff.deleted_quorums { + ui.label(format!( + "Quorum {} Type: {}", + deleted_quorum.quorum_hash, + QuorumType::from(deleted_quorum.llmq_type as u32) + )); + } + }); + + ui.separator(); + + // Quorums ChainLock Signatures + ui.heading("Quorums ChainLock Signatures"); + ui.label(format!( + "Total ChainLock Signatures: {}", + mn_list_diff.quorums_chainlock_signatures.len() + )); + + ScrollArea::vertical() + .id_salt("render_selected_mn_list_diff_5") + .show(ui, |ui| { + for (i, cl_sig) in mn_list_diff.quorums_chainlock_signatures.iter().enumerate() { + ui.label(format!( + "Signature {}: {} for indexes [{}]", + i, + hex::encode(cl_sig.signature), + cl_sig + .index_set + .iter() + .map(|index| index.to_string()) + .collect::>() + .join("-") + )); + } + }); + } + + fn render_quorum_snapshots(&mut self, ui: &mut Ui, qr_info: &QRInfo) { + let snapshots = [ + ("Quorum Snapshot h-c", &qr_info.quorum_snapshot_at_h_minus_c), + ( + "Quorum Snapshot h-2c", + &qr_info.quorum_snapshot_at_h_minus_2c, + ), + ( + "Quorum Snapshot h-3c", + &qr_info.quorum_snapshot_at_h_minus_3c, + ), + ]; + + if let Some((qs4c, _)) = &qr_info.quorum_snapshot_and_mn_list_diff_at_h_minus_4c { + snapshots.iter().for_each(|(name, snapshot)| { + if ui + .selectable_label(self.selected_qr_list_index == Some(name.to_string()), *name) + .clicked() + { + self.selected_qr_list_index = Some(name.to_string()); + self.selected_qr_item = + Some(SelectedQRItem::SelectedSnapshot((*snapshot).clone())); + } + }); + + if ui + .selectable_label( + self.selected_qr_list_index == Some("Quorum Snapshot h-4c".to_string()), + "Quorum Snapshot h-4c", + ) + .clicked() + { + self.selected_qr_list_index = Some("Quorum Snapshot h-4c".to_string()); + self.selected_qr_item = Some(SelectedQRItem::SelectedSnapshot((*qs4c).clone())); + } + } + } + + fn render_selected_quorum_entry(ui: &mut Ui, qualified_quorum_entry: &QualifiedQuorumEntry) { + ui.heading("Quorum Entry Details"); + + // General Quorum Info + ui.label(format!( + "Version: {}\nQuorum Type: {}\nQuorum Hash: {}", + qualified_quorum_entry.quorum_entry.version, + QuorumType::from(qualified_quorum_entry.quorum_entry.llmq_type as u32), + qualified_quorum_entry.quorum_entry.quorum_hash + )); + + ui.label(format!( + "Quorum Index: {}", + qualified_quorum_entry + .quorum_entry + .quorum_index + .map_or("None".to_string(), |idx| idx.to_string()) + )); + + ui.separator(); + + // **Additional Qualified Quorum Entry Information** + ui.heading("Quorum Verification Details"); + let verification_symbol = match &qualified_quorum_entry.verified { + LLMQEntryVerificationStatus::Verified => "✔ Verified".to_string(), + LLMQEntryVerificationStatus::Invalid(reason) => format!("❌ Invalid ({})", reason), + LLMQEntryVerificationStatus::Unknown => "⬜ Unknown".to_string(), + LLMQEntryVerificationStatus::Skipped(reason) => format!("⬜ Skipped ({})", reason), + }; + ui.label(format!("Verification Status: {}", verification_symbol)); + + ui.separator(); + + ui.heading("Commitment & Entry Hashes"); + ScrollArea::vertical() + .id_salt("commitment_entry_hash") + .show(ui, |ui| { + ui.label(format!( + "Commitment Hash: {}", + qualified_quorum_entry.commitment_hash + )); + ui.label(format!("Entry Hash: {}", qualified_quorum_entry.entry_hash)); + }); + + ui.separator(); + + // Signers & Valid Members + ui.heading("Quorum Members"); + ui.label(format!( + "Total Signers: {}\nValid Members: {}", + qualified_quorum_entry + .quorum_entry + .signers + .iter() + .filter(|&&b| b) + .count(), + qualified_quorum_entry + .quorum_entry + .valid_members + .iter() + .filter(|&&b| b) + .count() + )); + + ScrollArea::vertical() + .id_salt("quorum_members_grid") + .show(ui, |ui| { + ui.label(format!( + "Total Signers: {}\nValid Members: {}", + qualified_quorum_entry + .quorum_entry + .signers + .iter() + .filter(|&&b| b) + .count(), + qualified_quorum_entry + .quorum_entry + .valid_members + .iter() + .filter(|&&b| b) + .count() + )); + + ui.separator(); + + ui.heading("Signers & Valid Members Grid"); + + egui::Grid::new("quorum_members_grid") + .num_columns(8) // Adjust based on UI width + .striped(true) + .show(ui, |ui| { + for (i, (is_signer, is_valid)) in qualified_quorum_entry + .quorum_entry + .signers + .iter() + .zip(qualified_quorum_entry.quorum_entry.valid_members.iter()) + .enumerate() + { + let text = match (*is_signer, *is_valid) { + (true, true) => "✔✔", + (true, false) => "✔❌", + (false, true) => "❌✔", + (false, false) => "❌❌", + }; + + let response = ui.label(text); + + // Tooltip on hover to show member index + if response.hovered() { + ui.ctx().debug_painter().text( + response.rect.center(), + egui::Align2::CENTER_CENTER, + format!("Member {}", i), + egui::FontId::proportional(14.0), + egui::Color32::BLUE, + ); + } + + // Create a new row every 8 members + if (i + 1) % 8 == 0 { + ui.end_row(); + } + } + }); + }); + + ui.separator(); + + // Quorum Public Key + ui.heading("Quorum Public Key"); + ScrollArea::vertical() + .id_salt("render_selected_quorum_entry_2") + .show(ui, |ui| { + ui.label(format!( + "Public Key: {}", + qualified_quorum_entry.quorum_entry.quorum_public_key + )); + }); + + ui.separator(); + + // Quorum Verification Vector Hash + ui.heading("Verification Vector Hash"); + ui.label(format!( + "Quorum VVec Hash: {}", + qualified_quorum_entry.quorum_entry.quorum_vvec_hash + )); + + ui.separator(); + + // Threshold Signature + ui.heading("Threshold Signature"); + ScrollArea::vertical() + .id_salt("render_selected_quorum_entry_3") + .show(ui, |ui| { + ui.label(format!( + "Signature: {}", + hex::encode(qualified_quorum_entry.quorum_entry.threshold_sig.to_bytes()) + )); + }); + + ui.separator(); + + // Aggregated Signature + ui.heading("All Commitment Aggregated Signature"); + ScrollArea::vertical() + .id_salt("render_selected_quorum_entry_4") + .show(ui, |ui| { + ui.label(format!( + "Signature: {}", + hex::encode( + qualified_quorum_entry + .quorum_entry + .all_commitment_aggregated_signature + .to_bytes() + ) + )); + }); + } + + fn show_mn_list_diff_heights_as_string( + &mut self, + mn_list_diff: &MnListDiff, + last_diff: Option<&MnListDiff>, + ) -> String { + let base_height_as_string = match self.get_height_and_cache(&mn_list_diff.base_block_hash) { + Ok(height) => height.to_string(), + Err(_) => "?".to_string(), + }; + + let height = self.get_height_and_cache(&mn_list_diff.block_hash).ok(); + + let height_as_string = match height { + Some(height) => height.to_string(), + None => "?".to_string(), + }; + + let extra_block_diff_info = height + .and_then(|height| { + last_diff.and_then(|diff| { + self.get_height(&diff.block_hash) + .ok() + .and_then(|start_height| { + height + .checked_sub(start_height) + .map(|diff| format!(" (+ {})", diff)) + }) + }) + }) + .unwrap_or_default(); + + format!( + "{} -> {}{}", + base_height_as_string, height_as_string, extra_block_diff_info + ) + } + + fn render_mn_list_diffs(&mut self, ui: &mut Ui, qr_info: &QRInfo) { + let mn_diffs = [ + ( + format!( + "MNListDiff h-3c {}", + self.show_mn_list_diff_heights_as_string( + &qr_info.mn_list_diff_at_h_minus_3c, + qr_info + .quorum_snapshot_and_mn_list_diff_at_h_minus_4c + .as_ref() + .map(|(_, diff)| diff) + ) + ), + &qr_info.mn_list_diff_at_h_minus_3c, + ), + ( + format!( + "MNListDiff h-2c {}", + self.show_mn_list_diff_heights_as_string( + &qr_info.mn_list_diff_at_h_minus_2c, + Some(&qr_info.mn_list_diff_at_h_minus_3c) + ) + ), + &qr_info.mn_list_diff_at_h_minus_2c, + ), + ( + format!( + "MNListDiff h-c {}", + self.show_mn_list_diff_heights_as_string( + &qr_info.mn_list_diff_at_h_minus_c, + Some(&qr_info.mn_list_diff_at_h_minus_2c) + ) + ), + &qr_info.mn_list_diff_at_h_minus_c, + ), + ( + format!( + "MNListDiff h {}", + self.show_mn_list_diff_heights_as_string( + &qr_info.mn_list_diff_h, + Some(&qr_info.mn_list_diff_at_h_minus_c) + ) + ), + &qr_info.mn_list_diff_h, + ), + ( + format!( + "MNListDiff Tip {}", + self.show_mn_list_diff_heights_as_string( + &qr_info.mn_list_diff_tip, + Some(&qr_info.mn_list_diff_h) + ) + ), + &qr_info.mn_list_diff_tip, + ), + ]; + if let Some((_, mn_diff4c)) = &qr_info.quorum_snapshot_and_mn_list_diff_at_h_minus_4c { + let string = format!( + "MNListDiff h-4c {}", + self.show_mn_list_diff_heights_as_string(mn_diff4c, None) + ); + + if ui + .selectable_label( + self.selected_qr_list_index == Some(string.clone()), + string.as_str(), + ) + .clicked() + { + self.selected_qr_list_index = Some(string); + self.selected_qr_item = + Some(SelectedQRItem::MNListDiff(Box::new((*mn_diff4c).clone()))); + } + } + + mn_diffs.iter().for_each(|(name, diff)| { + if ui + .selectable_label(self.selected_qr_list_index == Some(name.to_string()), name) + .clicked() + { + self.selected_qr_list_index = Some(name.to_string()); + self.selected_qr_item = Some(SelectedQRItem::MNListDiff(Box::new((*diff).clone()))); + } + }); + } + + fn render_last_commitments(&mut self, ui: &mut Ui, cycle_hash: Option) { + let Some(cycle_hash) = cycle_hash else { + ui.label("QR Info had no rotated quorums. This should not happen."); + return; + }; + let Some(cycle_quorums) = self + .masternode_list_engine + .rotated_quorums_per_cycle + .get(&cycle_hash) + else { + ui.label(format!( + "Engine does not know of cycle {} at height {}, we know of cycles [{}]", + cycle_hash, + self.get_height_or_error_as_string(&cycle_hash), + self.masternode_list_engine + .rotated_quorums_per_cycle + .keys() + .map(|key| format!("{}, {}", self.get_height_or_error_as_string(key), key)) + .join(", ") + )); + return; + }; + if cycle_quorums.is_empty() { + ui.label(format!( + "Engine does not contain any rotated quorums for cycle {}", + cycle_hash + )); + } + for (index, commitment) in cycle_quorums.iter().enumerate() { + // Determine the appropriate symbol based on verification status + let verification_symbol = match commitment.verified { + LLMQEntryVerificationStatus::Verified => "✔", // Checkmark + LLMQEntryVerificationStatus::Invalid(_) => "❌", // Cross + LLMQEntryVerificationStatus::Unknown | LLMQEntryVerificationStatus::Skipped(_) => { + "⬜" + } // Box + }; + + let label_text = format!("{} Quorum at Index {}", verification_symbol, index); + + if ui + .selectable_label( + self.selected_qr_list_index == Some(index.to_string()), + label_text, + ) + .clicked() + { + self.selected_qr_list_index = Some(index.to_string()); + self.selected_qr_item = + Some(SelectedQRItem::QuorumEntry(Box::new(commitment.clone()))); + } + } + } + + fn render_quorum_snapshot_list(&mut self, ui: &mut Ui, qr_info: &QRInfo) { + for (index, snapshot) in qr_info.quorum_snapshot_list.iter().enumerate() { + if ui + .selectable_label( + self.selected_qr_list_index == Some(index.to_string()), + format!("Snapshot {}", index), + ) + .clicked() + { + self.selected_qr_list_index = Some(index.to_string()); + self.selected_qr_item = Some(SelectedQRItem::SelectedSnapshot(snapshot.clone())); + } + } + } + + fn render_mn_list_diff_list(&mut self, ui: &mut Ui, qr_info: &QRInfo) { + for (index, diff) in qr_info.mn_list_diff_list.iter().enumerate() { + if ui + .selectable_label( + self.selected_qr_list_index == Some(index.to_string()), + format!("MNListDiff {}", index), + ) + .clicked() + { + self.selected_qr_list_index = Some(index.to_string()); + self.selected_qr_item = Some(SelectedQRItem::MNListDiff(Box::new(diff.clone()))); + } + } + } + + fn render_quorums(&mut self, ui: &mut Ui) { + ui.heading("Quorum Viewer"); + + // Get all available quorum types + let quorum_types: Vec = self + .masternode_list_engine + .quorum_statuses + .keys() + .cloned() + .collect(); + + // Ensure a quorum type is selected + if self.selected_quorum_type_in_quorum_viewer.is_none() { + self.selected_quorum_type_in_quorum_viewer = quorum_types.first().copied(); + } + + // Render quorum type selection bar + ui.horizontal(|ui| { + for quorum_type in &quorum_types { + if ui + .selectable_label( + self.selected_quorum_type_in_quorum_viewer == Some(*quorum_type), + quorum_type.to_string(), + ) + .clicked() + { + self.selected_quorum_type_in_quorum_viewer = Some(*quorum_type); + self.selected_quorum_hash_in_quorum_viewer = None; // Reset selected quorum when switching types + } + } + }); + + ui.separator(); + + let Some(selected_quorum_type) = self.selected_quorum_type_in_quorum_viewer else { + ui.label("No quorum types available."); + return; + }; + + let Some(quorum_map) = self + .masternode_list_engine + .quorum_statuses + .get(&selected_quorum_type) + else { + ui.label("No quorums found for this type."); + return; + }; + + // Create a horizontal layout to align quorum hashes on the left and heights on the right + ui.horizontal(|ui| { + // Left Column: Quorum Hashes + ui.allocate_ui_with_layout( + egui::Vec2::new(500.0, 800.0), + Layout::top_down(Align::Min), + |ui| { + ui.heading(format!("Quorums of Type: {}", selected_quorum_type)); + + ScrollArea::vertical() + .id_salt("quorum_hashes_scroll") + .show(ui, |ui| { + egui::Grid::new("quorum_hashes_grid") + .num_columns(2) // Two columns: Quorum Hash | Status + .striped(true) + .show(ui, |ui| { + ui.label("Quorum Hash"); + ui.label("Status"); + ui.end_row(); + + for (quorum_hash, (_, _, status)) in quorum_map { + let hash_label = format!("{}", quorum_hash); + + // Display quorum hash as selectable + let hash_response = ui.selectable_label( + self.selected_quorum_hash_in_quorum_viewer + == Some(*quorum_hash), + hash_label, + ); + + if hash_response.clicked() { + self.selected_quorum_hash_in_quorum_viewer = + Some(*quorum_hash); + } + + // Determine status symbol + let (status_symbol, tooltip_text) = match status { + LLMQEntryVerificationStatus::Verified => ("✔", None), + LLMQEntryVerificationStatus::Invalid(reason) => { + ("❌", Some(reason.to_string())) + } + LLMQEntryVerificationStatus::Unknown => ("⬜", None), + LLMQEntryVerificationStatus::Skipped(reason) => { + ("⚠", Some(reason.to_string())) + } + }; + + // Display small status icon + let status_response = ui.label(status_symbol); + + // Show tooltip on hover if there's an error message + if let Some(tooltip) = tooltip_text + && status_response.hovered() + { + ui.ctx().debug_painter().text( + status_response.rect.center(), + egui::Align2::CENTER_CENTER, + tooltip, + egui::FontId::proportional(14.0), + egui::Color32::RED, + ); + } + + ui.end_row(); + } + }); + }); + }, + ); + + ui.separator(); + + // Right Column: Heights where selected quorum exists + ui.allocate_ui_with_layout( + Vec2::new(500.0, 800.0), + Layout::top_down(Align::Min), + |ui| { + ui.heading("Quorum Heights"); + + if let Some(selected_quorum_hash) = self.selected_quorum_hash_in_quorum_viewer { + if let Some((heights, key, status)) = quorum_map.get(&selected_quorum_hash) + { + ui.label(format!("Public Key: {}", key)); + ui.label(format!("Verification Status: {}", status)); + ScrollArea::vertical() + .id_salt("quorum_heights_scroll") + .show(ui, |ui| { + for height in heights { + ui.label(format!("Height: {}", height)); + } + }); + } else { + ui.label("Selected quorum not found."); + } + } else { + ui.label("Select a quorum to see its heights."); + } + }, + ); + }); + } + + #[allow(dead_code)] + fn render_selected_item_details(&mut self, ui: &mut Ui, selected_item: String) { + ui.heading("Details"); + + ScrollArea::vertical().show(ui, |ui| { + ui.monospace(selected_item); + }); + } + + /// Render core items, including chain-locked blocks and instant send transactions. + fn render_core_items(&mut self, ui: &mut Ui) { + ui.heading("Core Items Viewer"); + + // Layout: Left (ChainLocked Blocks), Middle (InstantSend Transactions), Right (Details) + ui.horizontal(|ui| { + // Left Column: Chain Locked Blocks + ui.allocate_ui_with_layout( + Vec2::new(200.0, 1000.0), + Layout::top_down(Align::Min), + |ui| { + ui.heading("ChainLocked Blocks"); + + ScrollArea::vertical().id_salt("chain_locked_blocks_scroll").show(ui, |ui| { + for (block_height, (block, chain_lock, is_valid)) in + self.chain_locked_blocks.iter() + { + let label_text = format!( + "{} {} {}", + if *is_valid { "✔" } else { "❌" }, + block_height, + block.header.block_hash() + ); + + if ui + .selectable_label( + matches!(self.selected_core_item, Some((CoreItem::ChainLockedBlock(_, ref l), _)) if l.block_height == *block_height), + label_text, + ) + .clicked() + { + self.selected_core_item = Some((CoreItem::ChainLockedBlock(block.clone(), chain_lock.clone()), *is_valid)); + } + } + }); + }, + ); + + ui.separator(); + + // Middle Column: Instant Send Transactions + ui.allocate_ui_with_layout( + egui::Vec2::new(300.0, 1000.0), + Layout::top_down(Align::Min), + |ui| { + ui.heading("Instant Send Transactions"); + + ScrollArea::vertical().id_salt("instant_send_scroll").show(ui, |ui| { + for (transaction, instant_lock, is_valid) in + self.instant_send_transactions.iter() + { + let label_text = format!( + "{} TxID: {}", + if *is_valid { "✔" } else { "❌" }, + transaction.txid() + ); + + if ui + .selectable_label( + matches!(self.selected_core_item, Some((CoreItem::InstantLockedTransaction(ref t, _, _), _)) if t == transaction), + label_text, + ) + .clicked() + { + self.selected_core_item = Some((CoreItem::InstantLockedTransaction(transaction.clone(), vec![], instant_lock.clone()), *is_valid)); + } + } + }); + }, + ); + + ui.separator(); + + // Right Column: Details of the Selected Item + ui.allocate_ui_with_layout( + egui::Vec2::new(ui.available_width(), ui.available_height()), + Layout::top_down(Align::Min), + |ui| { + if let Some((selected_core_item, _)) = &self.selected_core_item { + match selected_core_item { + CoreItem::ChainLockedBlock(..) => self.render_chain_lock_details(ui), + CoreItem::InstantLockedTransaction(..) => self.render_instant_send_details(ui), + _ => { + ui.label("Select an item to view details."); + }, + } + } else { + ui.label("Select an item to view details."); + } + }, + ); + }); + } + + /// Render details of a selected ChainLock + fn render_chain_lock_details(&mut self, ui: &mut Ui) { + ui.heading("ChainLock Details"); + + if let Some((CoreItem::ChainLockedBlock(block, chain_lock), is_valid)) = + &self.selected_core_item + { + ui.label(format!( + "Block Height: {}\nBlock Hash: {}\nValid: {}", + chain_lock.block_height, + chain_lock.block_hash, + if *is_valid { "✔ Yes" } else { "❌ No" }, + )); + + ui.separator(); + + ui.heading("Block Transactions"); + ScrollArea::vertical() + .id_salt("block_tx_scroll") + .show(ui, |ui| { + if block.txdata.is_empty() { + ui.label("No transactions in this block."); + } else { + for transaction in &block.txdata { + ui.label(format!("TxID: {}", transaction.txid())); + } + } + }); + + ui.separator(); + ui.heading("Quorum Signature"); + ui.label(format!( + "Signature: {}", + hex::encode(chain_lock.signature.to_bytes()) + )); + + //todo clean this + let b = serialize2(chain_lock); + let chain_lock_2: ChainLock2 = deserialize(b.as_slice()).expect("todo"); + match self + .masternode_list_engine + .chain_lock_potential_quorum_under(&chain_lock_2) + { + Ok(Some(quorum)) => { + ui.label(format!("Quorum Hash: {}", quorum.quorum_entry.quorum_hash,)); + ui.label(format!( + "Request Id: {}", + chain_lock.request_id().expect("expected request id") + )); + let sign_id = chain_lock_2 + .sign_id( + quorum.quorum_entry.llmq_type, + quorum.quorum_entry.quorum_hash, + None, + ) + .expect("expected sign id"); + ui.label(format!("Sign Hash (Sign ID): {}", sign_id)); + if let Err(e) = quorum + .verify_message_digest(sign_id.to_byte_array(), chain_lock_2.signature) + { + ui.label(format!("Signature Verification Error: {}", e)); + } + } + Ok(None) => { + ui.label("No quorum".to_string()); + } + Err(err) => { + ui.label(format!("Error finding quorum: {}", err)); + } + }; + + ui.separator(); + + ui.heading("Data"); + + ui.label(format!("Block Data {}", hex::encode(serialize2(block)),)); + + ui.label(format!("Lock Data {}", hex::encode(serialize2(chain_lock)),)); + + ui.separator(); + } else { + ui.label("No ChainLock selected."); + } + } + + /// Render details of a selected Instant Send transaction + fn render_instant_send_details(&mut self, ui: &mut Ui) { + ui.heading("Instant Send Details"); + + if let Some((CoreItem::InstantLockedTransaction(transaction, _, instant_lock), is_valid)) = + &self.selected_core_item + { + ui.label(format!( + "TxID: {}\nValid: {}\nCycle Hash:{}", + transaction.txid(), + if *is_valid { "✔ Yes" } else { "❌ No" }, + instant_lock.cyclehash, + )); + + ui.separator(); + + ui.heading("Transaction Inputs"); + ScrollArea::vertical() + .id_salt("tx_inputs_scroll") + .show(ui, |ui| { + if transaction.input.is_empty() { + ui.label("No inputs."); + } else { + for txin in &transaction.input { + ui.label(format!( + "Input: {}:{}", + txin.previous_output.txid, txin.previous_output.vout + )); + } + } + }); + + ui.separator(); + ui.heading("Transaction Outputs"); + ScrollArea::vertical() + .id_salt("tx_outputs_scroll") + .show(ui, |ui| { + if transaction.output.is_empty() { + ui.label("No outputs."); + } else { + for txout in &transaction.output { + ui.label(format!( + "Output: {} sat -> {}", + txout.value, txout.script_pubkey + )); + } + } + }); + + ui.separator(); + ui.heading("Signing Info"); + + //todo clean this + let b = serialize2(instant_lock); + let instant_lock_2: InstantLock2 = deserialize(b.as_slice()).expect("todo"); + match self.masternode_list_engine.is_lock_quorum(&instant_lock_2) { + Ok((quorum, request_sign_id, index)) => { + ui.label(format!( + "Quorum Hash: {} at index {}", + quorum.quorum_entry.quorum_hash, index, + )); + ui.label(format!("Request Id: {}", request_sign_id)); + let sign_id = instant_lock_2 + .sign_id( + quorum.quorum_entry.llmq_type, + quorum.quorum_entry.quorum_hash, + Some(request_sign_id), + ) + .expect("expected sign id"); + ui.label(format!("Sign Hash (Sign ID): {}", sign_id)); + if let Err(e) = quorum + .verify_message_digest(sign_id.to_byte_array(), instant_lock_2.signature) + { + ui.label(format!("Signature Verification Error: {}", e)); + } + } + Err(err) => { + ui.label(format!("Error finding quorum: {}", err)); + } + }; + + ui.separator(); + ui.heading("Quorum Signature"); + ui.label(format!( + "Signature: {}", + hex::encode(instant_lock.signature.to_bytes()) + )); + + ui.separator(); + + ui.heading("Data"); + + ui.label(format!( + "Transaction Data {}", + hex::encode(serialize2(transaction)), + )); + + ui.label(format!( + "Lock Data {}", + hex::encode(serialize2(instant_lock)), + )); + } else { + ui.label("No Instant Send transaction selected."); + } + } + + fn attempt_verify_chain_lock(&self, chain_lock: &ChainLock) -> bool { + let b = serialize2(chain_lock); + let chain_lock_2: ChainLock2 = deserialize(b.as_slice()).expect("todo"); + self.masternode_list_engine + .verify_chain_lock(&chain_lock_2) + .is_ok() + } + + fn attempt_verify_transaction_lock(&self, instant_lock: &InstantLock) -> bool { + let b = serialize2(instant_lock); + let instant_lock_2: InstantLock2 = deserialize(b.as_slice()).expect("todo"); + self.masternode_list_engine + .verify_is_lock(&instant_lock_2) + .is_ok() + } + + fn received_new_block(&mut self, block: Block, chain_lock: ChainLock) { + let valid = self.attempt_verify_chain_lock(&chain_lock); + self.end_block_height = chain_lock.block_height.to_string(); + if self.syncing + && let Some((base_block_height, masternode_list)) = self + .masternode_list_engine + .masternode_lists + .last_key_value() + && *base_block_height < chain_lock.block_height + { + let mut p2p_handler = match CoreP2PHandler::new(self.app_context.network, None) { + Ok(p2p_handler) => p2p_handler, + Err(e) => { + self.error = Some(e); + return; + } + }; + + let Some(qr_info) = self.fetch_rotated_quorum_info( + &mut p2p_handler, + masternode_list.block_hash, + chain_lock.block_hash.to_byte_array().into(), + ) else { + return; + }; + + self.feed_qr_info_and_get_dmls(qr_info, Some(p2p_handler)); + + // self.fetch_single_dml( + // &mut p2p_handler, + // masternode_list.block_hash, + // *base_block_height, + // BlockHash::from_byte_array(chain_lock.block_hash.to_byte_array()), + // chain_lock.block_height, + // true, + // ); + + // Reset selections when new data is loaded + self.selected_dml_diff_key = None; + self.selected_quorum_in_diff_index = None; + } + self.chain_locked_blocks + .insert(chain_lock.block_height, (block, chain_lock, valid)); + } +} + +impl ScreenLike for MasternodeListDiffScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Error => { + self.pending = None; + self.error = Some(message.to_string()); + } + MessageType::Success => { + self.message = Some((message.to_string(), message_type)); + } + MessageType::Info => { + // Do not show transient info messages to avoid noisy black text banners. + } + } + } + + fn display_task_result(&mut self, backend_task_success_result: BackendTaskSuccessResult) { + if let BackendTaskSuccessResult::CoreItem(core_item) = backend_task_success_result { + // println!("received core item {:?}", core_item); + match core_item { + CoreItem::InstantLockedTransaction(transaction, _, instant_lock) => { + let valid = self.attempt_verify_transaction_lock(&instant_lock); + self.instant_send_transactions + .push((transaction, instant_lock, valid)); + } + CoreItem::ChainLockedBlock(block, chain_lock) => { + self.received_new_block(block, chain_lock); + } + _ => {} + } + return; + } + match backend_task_success_result { + BackendTaskSuccessResult::MnListFetchedDiff { + base_height, + height, + diff, + } => { + // Apply to engine similarly to original UI method + if base_height == 0 && self.masternode_list_engine.masternode_lists.is_empty() { + match MasternodeListEngine::initialize_with_diff_to_height( + diff.clone(), + height, + self.app_context.network, + ) { + Ok(engine) => self.masternode_list_engine = engine, + Err(e) => self.error = Some(e.to_string()), + } + } else if let Err(e) = + self.masternode_list_engine + .apply_diff(diff.clone(), Some(height), false, None) + { + self.error = Some(e.to_string()); + } + self.mnlist_diffs.insert((base_height, height), diff); + // If this was the no-rotation path, queue the extra diffs needed for verification (restored behavior) + if matches!(self.pending, Some(PendingTask::DmlDiffNoRotation)) { + if let Some(task) = self.build_validation_diffs_task() { + self.queued_task = Some(task); + self.display_message( + "Fetched DMLs (no rotation); fetching validation diffs…", + MessageType::Info, + ); + } else if !self.masternode_list_engine.masternode_lists.is_empty() { + // Fallback: attempt verification directly + if let Err(e) = self + .masternode_list_engine + .verify_non_rotating_masternode_list_quorums( + height, + &[LLMQType::Llmqtype50_60, LLMQType::Llmqtype400_85], + ) + { + self.error = Some(e.to_string()); + } + self.pending = None; + self.display_message("Fetched DMLs (no rotation)", MessageType::Success); + } else { + self.pending = None; + self.display_message("Fetched DMLs (no rotation)", MessageType::Success); + } + } else { + self.pending = None; + self.display_message("Fetched DML diff", MessageType::Success); + } + self.selected_dml_diff_key = None; + self.selected_quorum_in_diff_index = None; + } + BackendTaskSuccessResult::MnListFetchedQrInfo { qr_info } => { + // Warm heights and cache diffs before feed_qr_info (replicates old flow) + self.insert_mn_list_diff(&qr_info.mn_list_diff_tip); + self.insert_mn_list_diff(&qr_info.mn_list_diff_h); + self.insert_mn_list_diff(&qr_info.mn_list_diff_at_h_minus_c); + self.insert_mn_list_diff(&qr_info.mn_list_diff_at_h_minus_2c); + self.insert_mn_list_diff(&qr_info.mn_list_diff_at_h_minus_3c); + if let Some((_, d)) = &qr_info.quorum_snapshot_and_mn_list_diff_at_h_minus_4c { + self.insert_mn_list_diff(d); + } + for d in &qr_info.mn_list_diff_list { + self.insert_mn_list_diff(d); + } + + // Apply to engine using the same closure as before to resolve heights + let block_height_cache = self.block_height_cache.clone(); + let app_context = self.app_context.clone(); + let get_height_fn = move |block_hash: &BlockHash| { + if block_hash.as_byte_array() == &[0; 32] { + return Ok(0); + } + if let Some(height) = block_height_cache.get(block_hash) { + return Ok(*height); + } + match app_context + .core_client + .read() + .unwrap() + .get_block_header_info( + &(BlockHash2::from_byte_array(block_hash.to_byte_array())), + ) { + Ok(block_info) => Ok(block_info.height as CoreBlockHeight), + Err(_) => Err(ClientDataRetrievalError::RequiredBlockNotPresent( + *block_hash, + )), + } + }; + if let Err(e) = self.masternode_list_engine.feed_qr_info( + qr_info.clone(), + false, + true, + Some(get_height_fn), + ) { + self.error = Some(e.to_string()); + } + // Store full qr_info for the QR tab + let key = qr_info.mn_list_diff_tip.block_hash; + self.qr_infos.insert(key, qr_info); + self.selected_dml_diff_key = None; + self.selected_quorum_in_diff_index = None; + // Queue extra diffs required for verification (previous behavior) + if let Some(task) = self.build_validation_diffs_task() { + self.queued_task = Some(task); + self.display_message( + "Fetched QR info + DMLs; fetching validation diffs…", + MessageType::Info, + ); + } else { + self.pending = None; + self.display_message("Fetched QR info + DMLs", MessageType::Success); + } + } + BackendTaskSuccessResult::MnListFetchedDiffs { items } => { + // Apply returned diffs sequentially + for ((base_h, h), diff) in items { + if base_h == 0 && self.masternode_list_engine.masternode_lists.is_empty() { + if let Ok(engine) = MasternodeListEngine::initialize_with_diff_to_height( + diff.clone(), + h, + self.app_context.network, + ) { + self.masternode_list_engine = engine; + } + } else { + let _ = self.masternode_list_engine.apply_diff( + diff.clone(), + Some(h), + false, + None, + ); + } + self.mnlist_diffs.insert((base_h, h), diff); + } + // Update rotating quorum heights cache (previous behavior) + let hashes = self + .masternode_list_engine + .latest_masternode_list_rotating_quorum_hashes(&[]); + for hash in &hashes { + if let Ok(height) = self.get_height_and_cache(hash) { + self.block_height_cache.insert(*hash, height); + } + } + // Verify non-rotating quorums as before + if let Some(latest_masternode_list) = + self.masternode_list_engine.latest_masternode_list() + && let Err(e) = self + .masternode_list_engine + .verify_non_rotating_masternode_list_quorums( + latest_masternode_list.known_height, + &[LLMQType::Llmqtype50_60, LLMQType::Llmqtype400_85], + ) + { + self.error = Some(e.to_string()); + } + self.pending = None; + self.display_message( + "Fetched validation diffs and verified non-rotating quorums", + MessageType::Success, + ); + } + BackendTaskSuccessResult::MnListChainLockSigs { entries } => { + for ((h, bh), sig) in entries { + self.chain_lock_sig_cache.insert((h, bh), sig); + if let Some(sig) = sig { + self.chain_lock_reversed_sig_cache + .entry(sig) + .or_default() + .insert((h, bh)); + } + } + self.pending = None; + self.display_message("Fetched chain lock signatures", MessageType::Success); + } + _ => {} + } + } + + fn refresh_on_arrival(&mut self) { + // Optionally refresh data when this screen is shown + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![("Tools", AppAction::None)], + vec![], + ); + + action |= add_left_panel( + ctx, + &self.app_context, + RootScreenType::RootScreenToolsMasternodeListDiffScreen, + ); + + action |= add_tools_subscreen_chooser_panel(ctx, self.app_context.as_ref()); + + // Styled central panel consistent with other tool screens; scroll only below tab row + action |= island_central_panel(ctx, |ui| { + // Top: input area (base/end block height + Get DMLs button) + let mut inner = AppAction::None; + inner |= self.render_input_area(ui); + // If we queued a backend task from a prior result processing, send it now + if let Some(task) = self.queued_task.take() { + inner |= AppAction::BackendTask(task); + } + + if let Some((msg, msg_type)) = self.message.clone() { + let dark_mode = ui.ctx().style().visuals.dark_mode; + let message_color = match msg_type { + MessageType::Error => Color32::from_rgb(255, 100, 100), + MessageType::Info => crate::ui::theme::DashColors::text_primary(dark_mode), + // Dark green for success text + MessageType::Success => Color32::DARK_GREEN, + }; + ui.horizontal(|ui| { + Frame::new() + .fill(message_color.gamma_multiply(0.1)) + .inner_margin(Margin::symmetric(10, 8)) + .corner_radius(5.0) + .stroke(egui::Stroke::new(1.0, message_color)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(msg).color(message_color)); + ui.add_space(10.0); + if ui.small_button("Dismiss").clicked() { + self.message = None; + } + }); + }); + }); + ui.add_space(10.0); + } + + if let Some(error_msg) = self.error.clone() { + let message_color = Color32::from_rgb(255, 100, 100); + ui.horizontal(|ui| { + Frame::new() + .fill(message_color.gamma_multiply(0.1)) + .inner_margin(Margin::symmetric(10, 8)) + .corner_radius(5.0) + .stroke(egui::Stroke::new(1.0, message_color)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(error_msg).color(message_color)); + ui.add_space(10.0); + if ui.small_button("Dismiss").clicked() { + self.error = None; + } + }); + }); + }); + ui.add_space(10.0); + } + + // Pending spinner (Dash Blue spinner, black text) + if let Some(p) = self.pending { + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.scope(|ui| { + let style = ui.style_mut(); + // Force spinner (fg stroke) to Dash Blue + style.visuals.widgets.inactive.fg_stroke.color = + crate::ui::theme::DashColors::DASH_BLUE; + style.visuals.widgets.active.fg_stroke.color = + crate::ui::theme::DashColors::DASH_BLUE; + style.visuals.widgets.hovered.fg_stroke.color = + crate::ui::theme::DashColors::DASH_BLUE; + ui.add(egui::Spinner::new()); + }); + let label = match p { + PendingTask::DmlDiffSingle => "Fetching DML diff…", + PendingTask::DmlDiffNoRotation => "Fetching DMLs (no rotation)…", + PendingTask::QrInfo => "Fetching QR info…", + PendingTask::QrInfoWithDmls => "Fetching QR info + DMLs…", + PendingTask::ChainLocks => "Fetching chain locks…", + }; + ui.colored_label(Color32::BLACK, label); + }); + ui.add_space(6.0); + } + + ui.separator(); + + self.render_selected_tab(ui); + inner + }); + action + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PendingTask { + DmlDiffSingle, + DmlDiffNoRotation, + QrInfo, + QrInfoWithDmls, + ChainLocks, +} diff --git a/src/ui/tools/mod.rs b/src/ui/tools/mod.rs index 76b8690be..02e084194 100644 --- a/src/ui/tools/mod.rs +++ b/src/ui/tools/mod.rs @@ -1,5 +1,7 @@ pub mod contract_visualizer_screen; pub mod document_visualizer_screen; +pub mod grovestark_screen; +pub mod masternode_list_diff_screen; pub mod platform_info_screen; pub mod proof_log_screen; pub mod proof_visualizer_screen; diff --git a/src/ui/tools/transition_visualizer_screen.rs b/src/ui/tools/transition_visualizer_screen.rs index ab1c05c8d..a05f39a1e 100644 --- a/src/ui/tools/transition_visualizer_screen.rs +++ b/src/ui/tools/transition_visualizer_screen.rs @@ -58,14 +58,13 @@ impl TransitionVisualizerScreen { match value { Value::Object(map) => { // Check if this is a contractBounds object with an id - if map.contains_key("type") && map.contains_key("id") { - if let (Some(Value::String(type_str)), Some(Value::String(id))) = + if map.contains_key("type") + && map.contains_key("id") + && let (Some(Value::String(type_str)), Some(Value::String(id))) = (map.get("type"), map.get("id")) - { - if type_str == "singleContract" { - ids.push(id.clone()); - } - } + && type_str == "singleContract" + { + ids.push(id.clone()); } // Recursively check all values for val in map.values() { @@ -241,12 +240,12 @@ impl TransitionVisualizerScreen { .as_secs(); self.broadcast_status = TransitionBroadcastStatus::Submitting(now); - if let Some(json) = &self.parsed_json { - if let Ok(state_transition) = serde_json::from_str(json) { - app_action = AppAction::BackendTask( - BackendTask::BroadcastStateTransition(state_transition), - ); - } + if let Some(json) = &self.parsed_json + && let Ok(state_transition) = serde_json::from_str(json) + { + app_action = AppAction::BackendTask( + BackendTask::BroadcastStateTransition(state_transition), + ); } } } diff --git a/src/ui/wallets/add_new_wallet_screen.rs b/src/ui/wallets/add_new_wallet_screen.rs index f993b5bc7..6ce596402 100644 --- a/src/ui/wallets/add_new_wallet_screen.rs +++ b/src/ui/wallets/add_new_wallet_screen.rs @@ -10,10 +10,10 @@ use crate::model::wallet::encryption::{DASH_SECRET_MESSAGE, encrypt_message}; use crate::model::wallet::{ClosedKeyItem, OpenWalletSeed, Wallet, WalletSeed}; use crate::ui::components::entropy_grid::U256EntropyGrid; use bip39::{Language, Mnemonic}; -use dash_sdk::dashcore_rpc::dashcore::bip32::{ChildNumber, DerivationPath}; use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; use dash_sdk::dpp::dashcore::Network; -use dash_sdk::dpp::dashcore::bip32::{ExtendedPrivKey, ExtendedPubKey}; +use dash_sdk::dpp::key_wallet::bip32::{ChildNumber, DerivationPath}; +use dash_sdk::dpp::key_wallet::bip32::{ExtendedPrivKey, ExtendedPubKey}; use eframe::emath::Align; use egui::{Color32, ComboBox, Direction, Frame, Grid, Layout, Margin, RichText, Stroke, Ui, Vec2}; use std::sync::atomic::Ordering; @@ -99,7 +99,6 @@ impl AddNewWalletScreen { let (encrypted_message, salt, nonce) = encrypt_message(DASH_SECRET_MESSAGE, self.password.as_str())?; self.app_context - .db .update_main_password(&salt, &nonce, &encrypted_message) .map_err(|e| e.to_string())?; } diff --git a/src/ui/wallets/import_wallet_screen.rs b/src/ui/wallets/import_wallet_screen.rs index d09f7aa4f..d2f37f169 100644 --- a/src/ui/wallets/import_wallet_screen.rs +++ b/src/ui/wallets/import_wallet_screen.rs @@ -12,10 +12,10 @@ use crate::ui::wallets::add_new_wallet_screen::{ DASH_BIP44_ACCOUNT_0_PATH_MAINNET, DASH_BIP44_ACCOUNT_0_PATH_TESTNET, }; use bip39::Mnemonic; -use dash_sdk::dashcore_rpc::dashcore::bip32::DerivationPath; use dash_sdk::dashcore_rpc::dashcore::key::Secp256k1; use dash_sdk::dpp::dashcore::Network; -use dash_sdk::dpp::dashcore::bip32::{ExtendedPrivKey, ExtendedPubKey}; +use dash_sdk::dpp::key_wallet::bip32::DerivationPath; +use dash_sdk::dpp::key_wallet::bip32::{ExtendedPrivKey, ExtendedPubKey}; use egui::{Color32, ComboBox, Direction, Grid, Layout, RichText, Stroke, Ui, Vec2}; use std::sync::atomic::Ordering; use std::sync::{Arc, RwLock}; @@ -63,7 +63,6 @@ impl ImportWalletScreen { let (encrypted_message, salt, nonce) = encrypt_message(DASH_SECRET_MESSAGE, self.password.as_str())?; self.app_context - .db .update_main_password(&salt, &nonce, &encrypted_message) .map_err(|e| e.to_string())?; } @@ -264,11 +263,10 @@ impl ScreenLike for ImportWalletScreen { Ok(mnemonic) => { self.seed_phrase = Some(mnemonic); // Clear any existing seed phrase error - if let Some(ref mut error) = self.error { - if error.contains("Invalid seed phrase") { + if let Some(ref mut error) = self.error + && error.contains("Invalid seed phrase") { self.error = None; } - } } Err(_) => { self.seed_phrase = None; @@ -278,20 +276,18 @@ impl ScreenLike for ImportWalletScreen { } else { // Clear seed phrase and error if not all words are filled self.seed_phrase = None; - if let Some(ref mut error) = self.error { - if error.contains("Invalid seed phrase") { + if let Some(ref mut error) = self.error + && error.contains("Invalid seed phrase") { self.error = None; } - } } // Display error message if seed phrase is invalid - if let Some(ref error_msg) = self.error { - if error_msg.contains("Invalid seed phrase") { + if let Some(ref error_msg) = self.error + && error_msg.contains("Invalid seed phrase") { ui.add_space(10.0); ui.colored_label(Color32::from_rgb(255, 100, 100), error_msg); } - } if self.seed_phrase.is_none() { return; diff --git a/src/ui/wallets/wallets_screen/mod.rs b/src/ui/wallets/wallets_screen/mod.rs index 5723a70af..99094982e 100644 --- a/src/ui/wallets/wallets_screen/mod.rs +++ b/src/ui/wallets/wallets_screen/mod.rs @@ -2,15 +2,18 @@ use crate::app::{AppAction, DesiredAppAction}; use crate::backend_task::BackendTask; use crate::backend_task::core::CoreTask; use crate::context::AppContext; -use crate::model::wallet::Wallet; +use crate::model::wallet::{Wallet, WalletSeedHash}; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; use crate::ui::theme::DashColors; use crate::ui::{MessageType, RootScreenType, ScreenLike, ScreenType}; use chrono::{DateTime, Utc}; use dash_sdk::dashcore_rpc::dashcore::{Address, Network}; -use dash_sdk::dpp::dashcore::bip32::{ChildNumber, DerivationPath}; +use dash_sdk::dpp::key_wallet::bip32::{ChildNumber, DerivationPath}; use eframe::egui::{self, ComboBox, Context, Ui}; use egui::{Color32, Frame, Margin, RichText}; use egui_extras::{Column, TableBuilder}; @@ -45,6 +48,12 @@ pub struct WalletsBalancesScreen { refreshing: bool, show_rename_dialog: bool, rename_input: String, + wallet_password: String, + show_password: bool, + error_message: Option, + remove_wallet_dialog: Option, + pending_wallet_removal: Option, + pending_wallet_removal_alias: Option, } pub trait DerivationPathHelpers { @@ -134,6 +143,12 @@ impl WalletsBalancesScreen { refreshing: false, show_rename_dialog: false, rename_input: String::new(), + wallet_password: String::new(), + show_password: false, + error_message: None, + remove_wallet_dialog: None, + pending_wallet_removal: None, + pending_wallet_removal_alias: None, } } @@ -144,10 +159,17 @@ impl WalletsBalancesScreen { wallet.receive_address(self.app_context.network, true, Some(&self.app_context)) }; - // Now the immutable borrow of `wallet` is dropped, and we can use `self` mutably - if let Err(e) = result { - self.display_message(&e, MessageType::Error); + match result { + Ok(address) => { + let message = format!("Added new receiving address: {}", address); + self.display_message(&message, MessageType::Success); + } + Err(e) => { + self.display_message(&e, MessageType::Error); + } } + } else { + self.display_message("No wallet selected", MessageType::Error); } } @@ -551,16 +573,132 @@ impl WalletsBalancesScreen { } fn render_bottom_options(&mut self, ui: &mut Ui) { + let wallet_is_open = self + .selected_wallet + .as_ref() + .is_some_and(|wallet_guard| wallet_guard.read().unwrap().is_open()); + if self.selected_filters.contains("Funds") { ui.add_space(10.0); - ui.horizontal(|ui| { - if ui - .button(RichText::new("➕ Add Receiving Address").size(14.0)) - .clicked() - { - self.add_receiving_address(); + + if wallet_is_open { + ui.horizontal(|ui| { + if ui + .button(RichText::new("➕ Add Receiving Address").size(14.0)) + .clicked() + { + self.add_receiving_address(); + } + }); + } else { + // Show wallet unlock UI for locked wallets when Funds filter is active + self.render_wallet_unlock_if_needed(ui); + } + } + + if self.selected_wallet.is_some() { + ui.add_space(16.0); + let dark_mode = ui.ctx().style().visuals.dark_mode; + + let remove_button = egui::Button::new( + RichText::new("🗑 Remove Wallet") + .color(Color32::WHITE) + .size(14.0), + ) + .min_size(egui::vec2(0.0, 28.0)) + .fill(DashColors::error_color(!dark_mode)) + .stroke(egui::Stroke::NONE) + .corner_radius(4.0); + + if ui.add(remove_button).clicked() + && let Some(selected_wallet) = &self.selected_wallet + { + let wallet = selected_wallet.read().unwrap(); + let alias = wallet + .alias + .clone() + .unwrap_or_else(|| "Unnamed Wallet".to_string()); + let seed_hash = wallet.seed_hash(); + drop(wallet); + + self.pending_wallet_removal = Some(seed_hash); + self.pending_wallet_removal_alias = Some(alias.clone()); + + let message = format!( + "Removing wallet \"{}\" will delete its local data, including addresses, balances, and asset locks stored on this device. Identities linked to it will remain but the keys derived from this wallet will no longer work unless the wallet is re-imported. Continue?", + alias + ); + + self.remove_wallet_dialog = Some( + ConfirmationDialog::new("Remove Wallet", message) + .confirm_text(Some("Remove")) + .cancel_text(Some("Cancel")) + .danger_mode(true), + ); + } + + if let Some(dialog) = self.remove_wallet_dialog.as_mut() { + let response = dialog.show(ui); + if let Some(status) = response.inner.dialog_response { + match status { + ConfirmationStatus::Confirmed => { + self.remove_wallet_dialog = None; + if let Some(seed_hash) = self.pending_wallet_removal.take() { + let alias = self + .pending_wallet_removal_alias + .take() + .unwrap_or_else(|| "Unnamed Wallet".to_string()); + self.handle_wallet_removal(seed_hash, alias); + } else { + self.pending_wallet_removal_alias = None; + } + } + ConfirmationStatus::Canceled => { + self.remove_wallet_dialog = None; + self.pending_wallet_removal = None; + self.pending_wallet_removal_alias = None; + } + } } - }); + } + } + } + + fn handle_wallet_removal(&mut self, seed_hash: WalletSeedHash, alias: String) { + match self.app_context.remove_wallet(&seed_hash) { + Ok(()) => { + let next_wallet = self + .app_context + .wallets + .read() + .ok() + .and_then(|wallets| wallets.values().next().cloned()); + + self.selected_wallet = next_wallet; + + if self.selected_wallet.is_none() { + self.selected_filters.clear(); + self.selected_filters.insert("Funds".to_string()); + } + + self.show_rename_dialog = false; + self.rename_input.clear(); + self.wallet_password.clear(); + self.show_password = false; + self.error_message = None; + self.refreshing = false; + + self.display_message( + &format!("Removed wallet \"{}\" successfully", alias), + MessageType::Success, + ); + } + Err(err) => { + self.display_message( + &format!("Failed to remove wallet: {}", err), + MessageType::Error, + ); + } } } @@ -725,15 +863,7 @@ impl WalletsBalancesScreen { } fn check_message_expiration(&mut self) { - if let Some((_, _, timestamp)) = &self.message { - let now = Utc::now(); - let elapsed = now.signed_duration_since(*timestamp); - - // Automatically dismiss the message after 10 seconds - if elapsed.num_seconds() >= 10 { - self.dismiss_message(); - } - } + // Messages no longer auto-expire, they must be dismissed manually } } @@ -799,8 +929,37 @@ impl ScreenLike for WalletsBalancesScreen { let mut inner_action = AppAction::None; let dark_mode = ui.ctx().style().visuals.dark_mode; + // Display messages at the top, outside of scroll area + let message = self.message.clone(); + if let Some((message, message_type, _timestamp)) = message { + let message_color = match message_type { + MessageType::Error => egui::Color32::from_rgb(255, 100, 100), + MessageType::Info => DashColors::text_primary(dark_mode), + MessageType::Success => egui::Color32::DARK_GREEN, + }; + + // Display message in a prominent frame + ui.horizontal(|ui| { + Frame::new() + .fill(message_color.gamma_multiply(0.1)) + .inner_margin(Margin::symmetric(10, 8)) + .corner_radius(5.0) + .stroke(egui::Stroke::new(1.0, message_color)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new(message).color(message_color)); + ui.add_space(10.0); + if ui.small_button("Dismiss").clicked() { + self.dismiss_message(); + } + }); + }); + }); + ui.add_space(10.0); + } + egui::ScrollArea::vertical() - .auto_shrink([false; 2]) + .auto_shrink([true; 2]) .show(ui, |ui| { if self.app_context.wallets.read().unwrap().is_empty() { self.render_no_wallets_view(ui); @@ -825,11 +984,12 @@ impl ScreenLike for WalletsBalancesScreen { }); }); - ui.add_space(10.0); - ui.separator(); ui.add_space(10.0); if self.selected_wallet.is_some() { + ui.separator(); + ui.add_space(10.0); + // Always show the filter selector ui.vertical(|ui| { ui.heading( @@ -865,41 +1025,9 @@ impl ScreenLike for WalletsBalancesScreen { ui.add_space(10.0); self.render_bottom_options(ui); - } else { - ui.vertical_centered(|ui| { - ui.add_space(50.0); - ui.label( - RichText::new("Please select a wallet to view its details") - .size(16.0) - .color(Color32::GRAY), - ); - }); } }); - let message = self.message.clone(); - if let Some((message, message_type, timestamp)) = message { - let message_color = match message_type { - MessageType::Error => egui::Color32::DARK_RED, - MessageType::Info => DashColors::text_primary(dark_mode), - MessageType::Success => egui::Color32::DARK_GREEN, - }; - - ui.add_space(25.0); // Same space as refreshing indicator - ui.horizontal(|ui| { - ui.add_space(10.0); - - // Calculate remaining seconds - let now = Utc::now(); - let elapsed = now.signed_duration_since(timestamp); - let remaining = (10 - elapsed.num_seconds()).max(0); - - // Add the message with auto-dismiss countdown - let full_msg = format!("{} ({}s)", message, remaining); - ui.label(egui::RichText::new(full_msg).color(message_color)); - }); - ui.add_space(2.0); // Same space below as refreshing indicator - } inner_action }); @@ -985,3 +1113,33 @@ impl ScreenLike for WalletsBalancesScreen { fn refresh(&mut self) {} } + +impl ScreenWithWalletUnlock for WalletsBalancesScreen { + fn selected_wallet_ref(&self) -> &Option>> { + &self.selected_wallet + } + + fn wallet_password_ref(&self) -> &String { + &self.wallet_password + } + + fn wallet_password_mut(&mut self) -> &mut String { + &mut self.wallet_password + } + + fn show_password(&self) -> bool { + self.show_password + } + + fn show_password_mut(&mut self) -> &mut bool { + &mut self.show_password + } + + fn set_error_message(&mut self, error_message: Option) { + self.error_message = error_message; + } + + fn error_message(&self) -> Option<&String> { + self.error_message.as_ref() + } +} diff --git a/src/utils/path.rs b/src/utils/path.rs index fed87451a..dbea509d0 100644 --- a/src/utils/path.rs +++ b/src/utils/path.rs @@ -6,24 +6,21 @@ use std::path::Path; /// - If the path ends with `.app/Contents/MacOS/Dash-Qt`, it displays as `Dash-Qt.app` /// - Otherwise, it displays the full path /// -/// # Examples -/// ``` -/// "/Applications/Dash-Qt.app/Contents/MacOS/Dash-Qt" -> "Dash-Qt.app" -/// "/usr/local/bin/dash-qt" -> "/usr/local/bin/dash-qt" -/// ``` +/// # Examples: +/// +/// * `"/Applications/Dash-Qt.app/Contents/MacOS/Dash-Qt" -> "Dash-Qt.app"` +/// * `"/usr/local/bin/dash-qt" -> "/usr/local/bin/dash-qt"` pub fn format_path_for_display(path: &Path) -> String { let path_str = path.to_string_lossy(); // Check if this is a macOS app bundle executable path - if cfg!(target_os = "macos") { - // Check if the path matches the pattern for an app bundle executable - if let Some(app_start) = path_str.rfind(".app/Contents/MacOS/") { - // Find the start of the app name by looking backwards for a path separator - let before_app = &path_str[..app_start]; - let app_name_start = before_app.rfind('/').map(|i| i + 1).unwrap_or(0); - let app_name = &path_str[app_name_start..app_start + 4]; // Include ".app" - return app_name.to_string(); - } + // Check if the path matches the pattern for an app bundle executable + if let Some(app_start) = path_str.rfind(".app/Contents/MacOS/") { + // Find the start of the app name by looking backwards for a path separator + let before_app = &path_str[..app_start]; + let app_name_start = before_app.rfind('/').map(|i| i + 1).unwrap_or(0); + let app_name = &path_str[app_name_start..app_start + 4]; // Include ".app" + return app_name.to_string(); } // For all other cases, return the full path