diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 15573b3a8..702940c60 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,19 +6,15 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Prepare repository - uses: actions/checkout@v3 - with: - flutter-version: '3.10.6' - channel: 'stable' + uses: actions/checkout@v4 - name: Install Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.16.0' + flutter-version: '3.19.6' channel: 'stable' - name: Setup | Rust - uses: ATiltedTree/setup-rust@v1 + uses: dtolnay/rust-toolchain@stable with: - rust-version: stable components: clippy - name: Checkout submodules run: git submodule update --init --recursive @@ -28,12 +24,7 @@ jobs: rustup target add x86_64-unknown-linux-gnu sudo apt clean sudo apt update - sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm - sudo apt install -y debhelper libclang-dev cargo rustc opencl-headers libssl-dev ocl-icd-opencl-dev - sudo apt install -y libc6-dev-i386 - sudo apt install -y build-essential cmake git libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev pkg-config llvm - sudo apt install -y build-essential debhelper cmake libclang-dev libncurses5-dev clang libncursesw5-dev cargo rustc opencl-headers libssl-dev pkg-config ocl-icd-opencl-dev - sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless + sudo apt install -y unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake openjdk-8-jre-headless libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm debhelper libclang-dev opencl-headers libssl-dev ocl-icd-opencl-dev libc6-dev-i386 - name: Build Lelantus run: | cd crypto_plugins/flutter_liblelantus/scripts/linux/ diff --git a/.gitmodules b/.gitmodules index 95b02e580..925be21c0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,4 +6,7 @@ url = https://github.com/cypherstack/flutter_libmonero.git [submodule "crypto_plugins/flutter_liblelantus"] path = crypto_plugins/flutter_liblelantus - url = https://github.com/cypherstack/flutter_liblelantus.git \ No newline at end of file + url = https://github.com/cypherstack/flutter_liblelantus.git +[submodule "crypto_plugins/frostdart"] + path = crypto_plugins/frostdart + url = https://github.com/cypherstack/frostdart diff --git a/analysis_options.yaml b/analysis_options.yaml index c5b4136b6..c363d17cd 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -90,6 +90,9 @@ linter: unawaited_futures: true avoid_double_and_int_checks: false constant_identifier_names: false + prefer_final_locals: true + prefer_final_in_for_each: true + require_trailing_commas: true # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule diff --git a/android/app/build.gradle b/android/app/build.gradle index ec8747bff..ae9570217 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 // ndkVersion = "21.1.6352462" // ndkVersion = "25.2.9519653" diff --git a/assets/default_themes/dark.zip b/assets/default_themes/dark.zip index eb20d7e6e..c6d417500 100644 Binary files a/assets/default_themes/dark.zip and b/assets/default_themes/dark.zip differ diff --git a/assets/default_themes/light.zip b/assets/default_themes/light.zip index 0048f661c..d94ce2ac8 100644 Binary files a/assets/default_themes/light.zip and b/assets/default_themes/light.zip differ diff --git a/assets/images/mascot.png b/assets/images/mascot.png new file mode 100644 index 000000000..9c05490a4 Binary files /dev/null and b/assets/images/mascot.png differ diff --git a/assets/svg/swap2.svg b/assets/svg/swap2.svg new file mode 100644 index 000000000..1c9ce8191 --- /dev/null +++ b/assets/svg/swap2.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index cef5d3aa8..19c76409e 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit cef5d3aa8c74c8dc9a466f803c964b243ad653a3 +Subproject commit 19c76409e55f1bfed58855eb767574604376edb6 diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index 9cd241b5e..b654bf448 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit 9cd241b5ea142e21c01dd7639b42603281c43287 +Subproject commit b654bf4488357c8a104900e11f9468d54a39f22b diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index cb876251b..2c684cedb 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit cb876251b97d20b12ddd05268913d2cf4b78f0bf +Subproject commit 2c684cedba6c3d9353c7ea748cadb5a246008027 diff --git a/crypto_plugins/frostdart b/crypto_plugins/frostdart new file mode 160000 index 000000000..d539de234 --- /dev/null +++ b/crypto_plugins/frostdart @@ -0,0 +1 @@ +Subproject commit d539de2348bdbb87bac341dcaa6a0755f21d48e2 diff --git a/docs/building.md b/docs/building.md index e7128df2e..0d88b1bb2 100644 --- a/docs/building.md +++ b/docs/building.md @@ -4,12 +4,27 @@ Here you will find instructions on how to install the necessary tools for buildi ## Prerequisites -- The only OS supported for building Android and Linux desktop is Ubuntu 20.04. Windows build are completed using Ubuntu 20.04 on WSL2. Advanced users may also be able to build on other Debian-based distributions like Linux Mint. +- The only OS supported for building Android and Linux desktop is Ubuntu 20.04. Windows builds require using Ubuntu 20.04 on WSL2. macOS builds for itself and iOS. Advanced users may also be able to build on other Debian-based distributions like Linux Mint. - Android setup ([Android Studio](https://developer.android.com/studio) and subsequent dependencies) - 100 GB of storage ## Linux host -The following instructions are for building and running on a Linux host. Alternatively, see the [Windows](#Windows host) section. + +The following instructions are for building and running on a Linux host. Alternatively, see the [Mac](#mac-host) and/or [Windows](#windows-host) section. This entire section (except for the Android Studio section) needs to be completed in WSL if building on a Windows host. + +### Flutter +Install Flutter 3.19.6 by [following their guide](https://docs.flutter.dev/get-started/install/linux/desktop?tab=download#install-the-flutter-sdk). You can also clone https://github.com/flutter/flutter, check out the `3.19.6` tag, and add its `flutter/bin` folder to your PATH as in +```sh +FLUTTER_DIR="$HOME/development/flutter" +git clone https://github.com/flutter/flutter.git "$FLUTTER_DIR" +cd "$FLUTTER_DIR" +git checkout 3.16.9 +echo 'export PATH="$PATH:'"$FLUTTER_DIR"'/bin"' >> "$HOME/.profile" +source "$HOME/.profile" +flutter precache +``` + +Run `flutter doctor` in a terminal to confirm its installation. ### Android Studio Install Android Studio. Follow instructions here [https://developer.android.com/studio/install#linux](https://developer.android.com/studio/install#linux) or install via snap: @@ -20,42 +35,46 @@ sudo snap install android-studio --classic ``` Use `Tools > SDK Manager` to install: - - `SDK Tools > Android SDK (API 30)` - - `SDK Tools > NDK` - `SDK Tools > Android SDK command line tools` - `SDK Tools > CMake` +and for Android builds, + - `SDK Tools > Android SDK (API 30)` + - `SDK Tools > NDK` Then in `File > Settings > Plugins`, install the **Flutter** and **Dart** plugins and restart the IDE. In `File > Settings > Languages & Frameworks > Flutter > Editor`, enable auto format on save to match the project's code style. If you have problems with the Dart SDK, make sure to run `flutter` in a terminal to download it (use `source ~/.bashrc` to update your environment variables if you're still using the same terminal from which you ran `setup.sh`). Run `flutter doctor` to install any missing dependencies and review and agree to any license agreements. -Make a Pixel 4 (API 30) x86_64 emulator with 2GB of storage space for emulation - -Install basic dependencies -``` -sudo apt-get install libssl-dev curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm python3-distutils -``` +Make a Pixel 4 (API 30) x86_64 emulator with 2GB of storage space for emulation. The following *may* be needed for Android studio: ``` sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2-1.0:i386 ``` +### Build dependencies +Install basic dependencies +``` +sudo apt-get install libssl-dev curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm python3-distutils +``` + Install [Rust](https://www.rust-lang.org/tools/install) with command: ``` curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.bashrc -rustup install 1.67.1 +rustup install 1.67.1 1.72.0 1.73.0 rustup default 1.67.1 ``` Install the additional components for Rust: ``` -cargo install cargo-ndk --version 2.12.7 +cargo install cargo-ndk --version 2.12.7 --locked ``` + Android specific dependencies: ``` sudo apt-get install libc6-dev-i386 rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android ``` + Linux desktop specific dependencies: ``` sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev meson python3-pip libgirepository1.0-dev valac xsltproc docbook-xsl @@ -67,18 +86,27 @@ After installing the prerequisites listed above, download the code and init the git clone https://github.com/cypherstack/stack_wallet.git cd stack_wallet git submodule update --init --recursive - ``` -Remove pre-installed system libraries for the following packages built by cryptography plugins in the crypto_plugins folder: `boost iconv libjson-dev libsecret openssl sodium unbound zmq`. You can use +Build the secure storage dependencies in order to target Linux (not needed for Windows or other platforms): ``` -sudo apt list --installed | grep boost +cd scripts/linux +./build_secure_storage_deps.sh +// when finished go back to the root directory +cd ../.. ``` -for example to find which pre-installed packages you may need to remove with `sudo apt remove`. Be careful, as some packages (especially boost) are linked to GNOME (GUI) packages: when in doubt, remove `-dev` packages first like with -``` -sudo apt-get remove '^libboost.*-dev.*' -``` - + +### Build coinlib +Coinlib's native secp256k1 library must be built prior to running Stack Wallet. It can be built from within the root `stack_wallet` folder on a... + - Linux host for Linux targets: `dart run coinlib:build_linux`, or + - Linux host for Windows targets: `dart run coinlib:build_windows_crosscompile` + - Windows host: `dart run coinlib:build_windows` + - WSL2 host: `dart run coinlib:build_wsl` + - macOS host: `dart run coinlib:build_macos` + +To build coinlib on Linux, you will need `docker` (see [installation instructions](https://docs.docker.com/engine/install/ubuntu/)) or [`podman`](https://podman.io/docs/installation) (`sudo apt-get -y install podman`) + +For Windows targets, you can use a `secp256k1.dll` produced by any of the three middle options if the first attempt doesn't succeed. ### Run prebuild script @@ -105,6 +133,19 @@ cd scripts/linux ./build_all.sh ``` +##### Remove system packages (may be needed for building flutter_libmonero) +[`flutter_libmonero`](https://github.com/cypherstack/flutter_libmonero) may have issues building due to conflicts with system packages: if so, follow this section. + +Remove pre-installed system libraries for the following packages built by cryptography plugins in the crypto_plugins folder: `boost iconv libjson-dev libsecret openssl sodium unbound zmq`. You can use +``` +sudo apt list --installed | grep boost +``` +for example to find which pre-installed packages you may need to remove with `sudo apt remove`. Be careful, as some packages (especially boost) are linked to GNOME (GUI) packages: when in doubt, remove `-dev` packages first like with +``` +sudo apt-get remove '^libboost.*-dev.*' +``` + + #### Building plugins for Windows ``` cd scripts/windows @@ -120,7 +161,7 @@ flutter pub get flutter run android ``` -Note on Emulators: Only x86_64 emulators are supported, x86 emulators will not work +Note on Emulators: Only x86_64 emulators are supported, x86 emulators will not work. You should [configure KVM](https://help.ubuntu.com/community/KVM/Installation) for much better performance. #### Linux Run the following commands or launch via Android Studio: @@ -129,20 +170,100 @@ flutter pub get flutter run linux ``` -## Windows host -### Visual Studio -Visual Studio is required for Windows development with the Flutter SDK. Download it at https://visualstudio.microsoft.com/downloads/ and install the "Desktop development with C++" workload, including all of its default components. +## Mac host -### Building libraries in WSL2 -Set up Ubuntu 20.04 in WSL2. Follow the entire Linux host section in the WSL2 Ubuntu 20.04 host to get set up to build. You will also need to install Rust and MXE dependencies on the WSL2 Ubuntu 20.04 host: - - [Install Rust](https://rustup.rs/) - ```sh - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - ``` - - Install MXE by running `stack_wallet/scripts/windows/deps.sh` - ```sh - ./stack_wallet/scripts/windows/deps.sh - ``` +### Dependencies +XCode, Homebrew and several homebrew packages, Rust, and Flutter are required for Mac development with the Flutter SDK. Multiple IDEs may work, but Android Studio is recommended. + +Download and install Xcode at https://developer.apple.com/xcode/, register your device (Mac or iPhone), and enable developer mode for your device as applicable. After installing XCode, make sure commandline tools are installed with `xcode-select --install`. + +Download and install [Homebrew](https://brew.sh/). The following command can install it via script: +``` +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +After installing Homebrew, install the following packages: +``` +brew install autoconf automake boost berkeley-db ca-certificates cbindgen cmake cmake cocoapods curl git libssh2 make openssl@1.1 openssl@3 perl pkg-config rustup-init sodium unbound unzip xz zmq +``` + +The following brew formula *may* be needed: +``` +brew install brotli cairo coreutils gdbm gettext glib gmp libevent libidn2 libnghttp2 libtool libunistring libx11 libxau libxcb libxdmcp libxext libxrender lzo m4 openldap pcre2 pixman procs rtmpdump tcl-tk xorgproto zstd +``` + + +Download and install [Rust](https://www.rust-lang.org/tools/install). [Rustup](https://rustup.rs/) is recommended for Rust setup. Use `rustc` to confirm successful installation. Install toolchains 1.67.1 and 1.72.0 and `cbindgen` and `cargo-lipo` too. You will also have to add the platform target(s) `aarch64-apple-ios` and/or `aarch64-apple-darwin`. You can use the command(s): +``` +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source ~/.bashrc +rustup install 1.67.1 +rustup install 1.72.0 +rustup default 1.67.1 +cargo install cbindgen cargo-lipo +rustup target add aarch64-apple-ios aarch64-apple-darwin +``` + +Optionally download [Android Studio](https://developer.android.com/studio) as an IDE and activate its Dart and Flutter plugins. VS Code may work as an alternative, but this is not recommended. + +### Flutter +Install [Flutter](https://docs.flutter.dev/get-started/install) 3.16.8 on your Mac host by following [these instructions](https://docs.flutter.dev/get-started/install/macos). Run `flutter doctor` in a terminal to confirm its installation. + +### Build plugins +#### Building plugins for iOS +``` +cd scripts/ios +./build_all.sh +``` + +#### Building plugins for macOS +``` +cd scripts/macos +./build_all.sh +``` + +### Run prebuild script +Certain test wallet parameter and API key template files must be created in order to run Stack Wallet. These can be created by script as in +``` +cd scripts +./prebuild.sh +// when finished go back to the root directory +cd .. +``` +or manually by creating the files referenced in that script with the specified content. + +### Running +#### iOS +Plug in your iOS device or use an emulato and then run the following commands: +``` +flutter pub get +flutter run ios +``` + +#### macOS +Run the following commands or launch via Android Studio: +``` +flutter pub get +flutter run macos +``` + +## Windows host + +### Visual Studio +Visual Studio is required for Windows development with the Flutter SDK. Download it at https://visualstudio.microsoft.com/downloads/ and install the "Desktop development with C++", "Linux development with C++", and "Visual C++ build tools" workloads. You may also need the Windows 10, 11, and/or Universal SDK workloads depending on your Windows version. + +### Build plugins in WSL2 +Set up Ubuntu 20.04 in WSL2. Follow the entire Linux host section in the WSL2 Ubuntu 20.04 host to get set up to build. The Android Studio section may be skipped in WSL (it's only needed on the Windows host). + +Install the following libraries: +``` +sudo apt-get install libgtk2.0-dev +``` + +You will also need to install MXE on the WSL2 Ubuntu 20.04 host and can do so by running `stack_wallet/scripts/windows/deps.sh`: +``` +./stack_wallet/scripts/windows/deps.sh +``` The WSL2 host may optionally be navigated to the `stack_wallet` repository on the Windows host in order to build the plugins in-place and skip the next section in which you copy the `dll`s from WSL2 to Windows. Then build windows `dll` libraries by running the following script on the WSL2 Ubuntu 20.04 host: @@ -158,10 +279,38 @@ Copy the resulting `dll`s to their respective positions on the Windows host: --> -### Install Flutter on Windows host -Install Flutter 3.10.3 on your Windows host (not in WSL2) by following these instructions: https://docs.flutter.dev/get-started/install/windows or by running `scripts/windows/deps.ps1`. You may still have to add `C:\development\flutter\bin` to PATH before proceeding, even if you ran `deps.ps1`. Run `flutter doctor` in PowerShell to confirm its installation. +Frostdart will be built by the Windows host later. -### Dependencies +### Install Flutter on Windows host +Install Flutter 3.19.6 on your Windows host (not in WSL2) by [following their guide](https://docs.flutter.dev/get-started/install/windows/desktop?tab=download#install-the-flutter-sdk) or by cloning https://github.com/flutter/flutter, checking out the `3.19.6` tag, and adding its `flutter/bin` folder to your PATH as in +```bat +@echo off +set "FLUTTER_DIR=%USERPROFILE%\development\flutter" +git clone https://github.com/flutter/flutter.git "%FLUTTER_DIR%" +cd /d "%FLUTTER_DIR%" +git checkout 3.16.9 +setx PATH "%PATH%;%FLUTTER_DIR%\bin" +echo Flutter setup completed. Please restart your command prompt. +``` + +Run `flutter doctor` in PowerShell to confirm its installation. + +### Rust +Install [Rust](https://www.rust-lang.org/tools/install) on the Windows host (not in WSL2). Download the installer from [rustup.rs](https://rustup.rs), make sure it works on the commandline (you may need to open a new terminal), and install the following versions: +``` +rustup install 1.72.0 # For frostdart and tor. +rustup install 1.67.1 # For flutter_libepiccash. +rustup default 1.67.1 +``` + + +### Windows SDK and Developer Mode Install the Windows SDK: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/ You may need to install the [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/), which can be installed [by Visual Studio](https://stackoverflow.com/a/73923899) (`Tools > Get Tools and Features... > Modify > Individual Components > Windows 10 SDK`). Enable Developer Mode for symlink support, @@ -179,14 +328,22 @@ or [download the package](https://www.nuget.org/packages/Microsoft.Windows.CppWi ### Run prebuild script -Certain test wallet parameter and API key template files must be created in order to run Stack Wallet. These can be created by script as in +Certain test wallet parameter and API key template files must be created in order to run Stack Wallet on Windows. These can be created by script using PowerShell on the Windows host as in ``` cd scripts ./prebuild.ps1 -// when finished go back to the root directory -cd .. +cd .. // When finished go back to the root directory. +``` +or manually by creating the files referenced in that script with the specified content. + +### Build frostdart + +In PowerShell on the Windows host, navigate to the `stack_wallet` folder: +``` +cd crypto_plugins/frostdart +./build_all.bat +cd .. // When finished go back to the root directory. ``` -or manually by creating the files referenced in that script with the specified content. ### Running @@ -195,3 +352,11 @@ Run the following commands: flutter pub get flutter run -d windows ``` + +# Troubleshooting + +Run with `-v` or `--verbose` to see a more detailed error. Certain exceptions (like missing a plugin library) may not report quality errors without `verbose`, especially on Windows. + +## Tor + +To test Tor usage, run Stack Wallet from Android Studio. Click the Flutter DevTools icon in the Run tab (next to the Hot Reload and Hot Restart buttons) and navigate to the Network tab. Connections using Tor will show as `GET InternetAddress('127.0.0.1', IPv4) 101 ws`. Connections outside of Tor will show the destination address directly (although some Tor requests may also show the destination address directly, check the Headers take for *eg.* `{localPort: 59940, remoteAddress: 127.0.0.1, remotePort: 6725}`. `localPort` should match your Tor port. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b2235ac27..fd6ae37e5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,6 +3,9 @@ PODS: - Flutter - MTBBarcodeScanner - SwiftProtobuf + - coinlib_flutter (0.3.2): + - Flutter + - FlutterMacOS - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift @@ -106,12 +109,15 @@ PODS: - Flutter - flutter_libmonero (0.0.1): - Flutter + - flutter_libsparkmobile (0.0.1): + - Flutter - flutter_local_notifications (0.0.1): - Flutter - flutter_native_splash (0.0.1): - Flutter - flutter_secure_storage (6.0.0): - Flutter + - frostdart (0.0.1) - integration_test (0.0.1): - Flutter - isar_flutter_libs (1.0.0): @@ -126,7 +132,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.0.4): + - permission_handler_apple (9.1.1): - Flutter - ReachabilitySwift (5.0.0) - SDWebImage (5.13.2): @@ -134,13 +140,12 @@ PODS: - SDWebImage/Core (5.13.2) - share_plus (0.0.1): - Flutter - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - stack_wallet_backup (0.0.1): - Flutter - SwiftProtobuf (1.19.0) - SwiftyGif (5.4.3) + - tor_ffi_plugin (0.0.1): + - Flutter - url_launcher_ios (0.0.1): - Flutter - wakelock (0.0.1): @@ -148,6 +153,7 @@ PODS: DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) + - coinlib_flutter (from `.symlinks/plugins/coinlib_flutter/darwin`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) @@ -158,9 +164,11 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_libepiccash (from `.symlinks/plugins/flutter_libepiccash/ios`) - flutter_libmonero (from `.symlinks/plugins/flutter_libmonero/ios`) + - flutter_libsparkmobile (from `.symlinks/plugins/flutter_libsparkmobile/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - frostdart (from `.symlinks/plugins/frostdart/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - lelantus (from `.symlinks/plugins/lelantus/ios`) @@ -169,8 +177,8 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - stack_wallet_backup (from `.symlinks/plugins/stack_wallet_backup/ios`) + - tor_ffi_plugin (from `.symlinks/plugins/tor_ffi_plugin/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) @@ -187,6 +195,8 @@ SPEC REPOS: EXTERNAL SOURCES: barcode_scan2: :path: ".symlinks/plugins/barcode_scan2/ios" + coinlib_flutter: + :path: ".symlinks/plugins/coinlib_flutter/darwin" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" cw_monero: @@ -207,12 +217,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_libepiccash/ios" flutter_libmonero: :path: ".symlinks/plugins/flutter_libmonero/ios" + flutter_libsparkmobile: + :path: ".symlinks/plugins/flutter_libsparkmobile/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + frostdart: + :path: ".symlinks/plugins/frostdart/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" isar_flutter_libs: @@ -229,10 +243,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" stack_wallet_backup: :path: ".symlinks/plugins/stack_wallet_backup/ios" + tor_ffi_plugin: + :path: ".symlinks/plugins/tor_ffi_plugin/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock: @@ -240,6 +254,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 + coinlib_flutter: 6abec900d67762a6e7ccfd567a3cd3ae00bbee35 connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a cw_monero: 9816991daff0e3ad0a8be140e31933b5526babd4 cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 @@ -249,30 +264,32 @@ SPEC CHECKSUMS: DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: ce3938a0df3cc1ef404671531facef740d03f920 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_libepiccash: 36241aa7d3126f6521529985ccb3dc5eaf7bb317 flutter_libmonero: da68a616b73dd0374a8419c684fa6b6df2c44ffe - flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 + flutter_libsparkmobile: 6373955cc3327a926d17059e7405dde2fb12f99f + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be + frostdart: 4c72b69ccac2f13ede744107db046a125acce597 integration_test: 13825b8a9334a850581300559b8839134b124670 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 lelantus: 417f0221260013dfc052cae9cf4b741b6479edba local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 - permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 - shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c stack_wallet_backup: 5b8563aba5d8ffbf2ce1944331ff7294a0ec7c03 SwiftProtobuf: 6ef3f0e422ef90d6605ca20b21a94f6c1324d6b3 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 + tor_ffi_plugin: d80e291b649379c8176e1be739e49be007d4ef93 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f PODFILE CHECKSUM: 57c8aed26fba39d3ec9424816221f294a07c58eb -COCOAPODS: 1.11.3 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fa0e9728d..7a1d5f01e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -193,7 +193,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -305,6 +305,7 @@ "${BUILT_PRODUCTS_DIR}/SwiftProtobuf/SwiftProtobuf.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/barcode_scan2/barcode_scan2.framework", + "${BUILT_PRODUCTS_DIR}/coinlib_flutter/secp256k1.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/cw_monero/cw_monero.framework", "${BUILT_PRODUCTS_DIR}/cw_shared_external/cw_shared_external.framework", @@ -313,9 +314,11 @@ "${BUILT_PRODUCTS_DIR}/devicelocale/devicelocale.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/flutter_libmonero/flutter_libmonero.framework", + "${PODS_ROOT}/../.symlinks/plugins/flutter_libsparkmobile/ios/flutter_libsparkmobile.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_native_splash/flutter_native_splash.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", + "${BUILT_PRODUCTS_DIR}/frostdart/frostdart.framework", "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", "${BUILT_PRODUCTS_DIR}/isar_flutter_libs/isar_flutter_libs.framework", "${BUILT_PRODUCTS_DIR}/lelantus/lelantus.framework", @@ -338,6 +341,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftProtobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/barcode_scan2.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/secp256k1.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_monero.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cw_shared_external.framework", @@ -346,9 +350,11 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/devicelocale.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_libmonero.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_libsparkmobile.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_splash.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/frostdart.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/isar_flutter_libs.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/lelantus.framework", diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a33..5e31d3d34 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Function() electrumAdapterUpdateCallback; static const minCacheConfirms = 30; - CachedElectrumXClient({ - required this.electrumXClient, - required this.electrumAdapterClient, - required this.electrumAdapterUpdateCallback, - }); + CachedElectrumXClient({required this.electrumXClient}); factory CachedElectrumXClient.from({ required ElectrumXClient electrumXClient, - required ElectrumClient electrumAdapterClient, - required Future Function() electrumAdapterUpdateCallback, }) => CachedElectrumXClient( electrumXClient: electrumXClient, - electrumAdapterClient: electrumAdapterClient, - electrumAdapterUpdateCallback: electrumAdapterUpdateCallback, ); - /// If the client is closed, use the callback to update it. - _checkElectrumAdapterClient() async { - if (electrumAdapterClient.peer.isClosed) { - Logging.instance.log( - "ElectrumAdapterClient is closed, reopening it...", - level: LogLevel.Info, - ); - ElectrumClient? _electrumAdapterClient = - await electrumAdapterUpdateCallback.call(); - electrumAdapterClient = _electrumAdapterClient; - } - } - Future> getAnonymitySet({ required String groupId, String blockhash = "", @@ -80,12 +54,9 @@ class CachedElectrumXClient { set = Map.from(cachedSet); } - await _checkElectrumAdapterClient(); - - final newSet = await (electrumAdapterClient as FiroElectrumClient) - .getLelantusAnonymitySet( + final newSet = await electrumXClient.getLelantusAnonymitySet( groupId: groupId, - blockHash: set["blockHash"] as String, + blockhash: set["blockHash"] as String, ); // update set with new data @@ -138,6 +109,7 @@ class CachedElectrumXClient { required String groupId, String blockhash = "", required Coin coin, + required bool useOnlyCacheIfNotEmpty, }) async { try { final box = await DB.instance.getSparkAnonymitySetCacheBox(coin: coin); @@ -155,12 +127,12 @@ class CachedElectrumXClient { }; } else { set = Map.from(cachedSet); + if (useOnlyCacheIfNotEmpty) { + return set; + } } - await _checkElectrumAdapterClient(); - - final newSet = await (electrumAdapterClient as FiroElectrumClient) - .getSparkAnonymitySet( + final newSet = await electrumXClient.getSparkAnonymitySet( coinGroupId: groupId, startBlockHash: set["blockHash"] as String, ); @@ -218,10 +190,11 @@ class CachedElectrumXClient { final cachedTx = box.get(txHash) as Map?; if (cachedTx == null) { - await _checkElectrumAdapterClient(); - final Map result = - await electrumAdapterClient.getTransaction(txHash); + await electrumXClient.getTransaction( + txHash: txHash, + verbose: verbose, + ); result.remove("hex"); result.remove("lelantusData"); @@ -263,10 +236,7 @@ class CachedElectrumXClient { cachedSerials.length - 100, // 100 being some arbitrary buffer ); - await _checkElectrumAdapterClient(); - - final serials = await (electrumAdapterClient as FiroElectrumClient) - .getLelantusUsedCoinSerials( + final serials = await electrumXClient.getLelantusUsedCoinSerials( startNumber: startNumber, ); @@ -314,22 +284,12 @@ class CachedElectrumXClient { cachedTags.length - 100, // 100 being some arbitrary buffer ); - await _checkElectrumAdapterClient(); - - final tags = - await (electrumAdapterClient as FiroElectrumClient).getUsedCoinsTags( + final newTags = await electrumXClient.getSparkUsedCoinsTags( startNumber: startNumber, ); - // final newSerials = List.from(serials["serials"] as List) - // .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e) - // .toSet(); - - // Convert the Map tags to a Set. - final newTags = (tags["tags"] as List).toSet(); - // ensure we are getting some overlap so we know we are not missing any - if (cachedTags.isNotEmpty && tags.isNotEmpty) { + if (cachedTags.isNotEmpty && newTags.isNotEmpty) { assert(cachedTags.intersection(newTags).isNotEmpty); } diff --git a/lib/electrumx_rpc/client_manager.dart b/lib/electrumx_rpc/client_manager.dart new file mode 100644 index 000000000..26db04b4b --- /dev/null +++ b/lib/electrumx_rpc/client_manager.dart @@ -0,0 +1,96 @@ +import 'dart:async'; + +import 'package:electrum_adapter/electrum_adapter.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +class ClientManager { + ClientManager._(); + static final ClientManager sharedInstance = ClientManager._(); + + final Map _map = {}; + final Map _heights = {}; + final Map> _subscriptions = {}; + final Map> _heightCompleters = {}; + + String _keyHelper(CryptoCurrency cryptoCurrency) { + return "${cryptoCurrency.runtimeType}_${cryptoCurrency.network.name}"; + } + + final Finalizer _finalizer = Finalizer((manager) async { + await manager._kill(); + }); + + ElectrumClient? getClient({ + required CryptoCurrency cryptoCurrency, + }) => + _map[_keyHelper(cryptoCurrency)]; + + void addClient( + ElectrumClient client, { + required CryptoCurrency cryptoCurrency, + }) { + final key = _keyHelper(cryptoCurrency); + if (_map[key] != null) { + throw Exception("ElectrumX Client for $key already exists."); + } else { + _map[key] = client; + } + + _heightCompleters[key] = Completer(); + _subscriptions[key] = client.subscribeHeaders().listen((event) { + _heights[key] = event.height; + + if (!_heightCompleters[key]!.isCompleted) { + _heightCompleters[key]!.complete(event.height); + } + }); + } + + Future getChainHeightFor(CryptoCurrency cryptoCurrency) async { + final key = _keyHelper(cryptoCurrency); + + if (_map[key] == null) { + throw Exception( + "No managed ElectrumClient for $key found.", + ); + } + if (_heightCompleters[key] == null) { + throw Exception( + "No managed _heightCompleters for $key found.", + ); + } + + return _heights[key] ?? await _heightCompleters[key]!.future; + } + + Future remove({ + required CryptoCurrency cryptoCurrency, + }) async { + final key = _keyHelper(cryptoCurrency); + await _subscriptions[key]?.cancel(); + _subscriptions.remove(key); + _heights.remove(key); + _heightCompleters.remove(key); + + return _map.remove(key); + } + + Future closeAll() async { + await _kill(); + _finalizer.detach(this); + } + + Future _kill() async { + for (final sub in _subscriptions.values) { + await sub.cancel(); + } + for (final client in _map.values) { + await client.close(); + } + + _heightCompleters.clear(); + _heights.clear(); + _subscriptions.clear(); + _map.clear(); + } +} diff --git a/lib/electrumx_rpc/electrumx_chain_height_service.dart b/lib/electrumx_rpc/electrumx_chain_height_service.dart deleted file mode 100644 index 3696e78f9..000000000 --- a/lib/electrumx_rpc/electrumx_chain_height_service.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'dart:async'; - -import 'package:electrum_adapter/electrum_adapter.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/logger.dart'; - -/// Manage chain height subscriptions for each coin. -abstract class ChainHeightServiceManager { - // A map of chain height services for each coin. - static final Map _services = {}; - // Map get services => _services; - - // Get the chain height service for a specific coin. - static ChainHeightService? getService(Coin coin) { - return _services[coin]; - } - - // Add a chain height service for a specific coin. - static void add(ChainHeightService service, Coin coin) { - // Don't add a new service if one already exists. - if (_services[coin] == null) { - _services[coin] = service; - } else { - throw Exception("Chain height service for $coin already managed"); - } - } - - // Remove a chain height service for a specific coin. - static void remove(Coin coin) { - _services.remove(coin); - } - - // Close all subscriptions and clean up resources. - static Future dispose() async { - // Close each subscription. - // - // Create a list of keys to avoid concurrent modification during iteration - var keys = List.from(_services.keys); - - // Iterate over the copy of the keys - for (final coin in keys) { - final ChainHeightService? service = getService(coin); - await service?.cancelListen(); - remove(coin); - } - } -} - -/// A service to fetch and listen for chain height updates. -/// -/// TODO: Add error handling and branching to handle various other scenarios. -class ChainHeightService { - // The electrum_adapter client to use for fetching chain height updates. - ElectrumClient client; - - // The subscription to listen for chain height updates. - StreamSubscription? _subscription; - - // Whether the service has started listening for updates. - bool get started => _subscription != null; - - // The current chain height. - int? _height; - int? get height => _height; - - // Whether the service is currently reconnecting. - bool _isReconnecting = false; - - // The reconnect timer. - Timer? _reconnectTimer; - - // The reconnection timeout duration. - static const Duration _connectionTimeout = Duration(seconds: 10); - - ChainHeightService({required this.client}); - - /// Fetch the current chain height and start listening for updates. - Future fetchHeightAndStartListenForUpdates() async { - // Don't start a new subscription if one already exists. - if (_subscription != null) { - throw Exception( - "Attempted to start a chain height service where an existing" - " subscription already exists!", - ); - } - - // A completer to wait for the current chain height to be fetched. - final completer = Completer(); - - // Fetch the current chain height. - _subscription = client.subscribeHeaders().listen((BlockHeader event) { - _height = event.height; - - if (!completer.isCompleted) { - completer.complete(_height); - } - }); - - _subscription?.onError((dynamic error) { - _handleError(error); - }); - - // Wait for the current chain height to be fetched. - return completer.future; - } - - /// Handle an error from the subscription. - void _handleError(dynamic error) { - Logging.instance.log( - "Error reconnecting for chain height: ${error.toString()}", - level: LogLevel.Error, - ); - - _subscription?.cancel(); - _subscription = null; - _attemptReconnect(); - } - - /// Attempt to reconnect to the electrum server. - void _attemptReconnect() { - // Avoid multiple reconnection attempts. - if (_isReconnecting) return; - _isReconnecting = true; - - // Attempt to reconnect. - unawaited(fetchHeightAndStartListenForUpdates().then((_) { - _isReconnecting = false; - })); - - // Set a timer to on the reconnection attempt and clean up if it fails. - _reconnectTimer?.cancel(); - _reconnectTimer = Timer(_connectionTimeout, () async { - if (_subscription == null) { - await _subscription?.cancel(); - _subscription = null; // Will also occur on an error via handleError. - _reconnectTimer?.cancel(); - _reconnectTimer = null; - _isReconnecting = false; - } - }); - } - - /// Stop listening for chain height updates. - Future cancelListen() async { - await _subscription?.cancel(); - _subscription = null; - _reconnectTimer?.cancel(); - } -} diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 98c6614f9..a5fcf5605 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -20,16 +20,17 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:mutex/mutex.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart'; -import 'package:stackwallet/electrumx_rpc/rpc.dart'; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/tor_service.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stream_channel/stream_channel.dart'; class WifiOnlyException implements Exception {} @@ -65,6 +66,8 @@ class ElectrumXNode { } class ElectrumXClient { + final CryptoCurrency cryptoCurrency; + String get host => _host; late String _host; @@ -74,14 +77,13 @@ class ElectrumXClient { bool get useSSL => _useSSL; late bool _useSSL; - JsonRPC? get rpcClient => _rpcClient; - JsonRPC? _rpcClient; - - StreamChannel? get electrumAdapterChannel => _electrumAdapterChannel; + // StreamChannel? get electrumAdapterChannel => _electrumAdapterChannel; StreamChannel? _electrumAdapterChannel; - ElectrumClient? get electrumAdapterClient => _electrumAdapterClient; - ElectrumClient? _electrumAdapterClient; + ElectrumClient? getElectrumAdapter() => + ClientManager.sharedInstance.getClient( + cryptoCurrency: cryptoCurrency, + ); late Prefs _prefs; late TorService _torService; @@ -91,9 +93,6 @@ class ElectrumXClient { final Duration connectionTimeoutForSpecialCaseJsonRPCClients; - Coin? get coin => _coin; - late Coin? _coin; - // add finalizer to cancel stream subscription when all references to an // instance of ElectrumX becomes inaccessible static final Finalizer _finalizer = Finalizer( @@ -114,7 +113,7 @@ class ElectrumXClient { required bool useSSL, required Prefs prefs, required List failovers, - Coin? coin, + required this.cryptoCurrency, this.connectionTimeoutForSpecialCaseJsonRPCClients = const Duration(seconds: 60), TorService? torService, @@ -125,7 +124,6 @@ class ElectrumXClient { _host = host; _port = port; _useSSL = useSSL; - _coin = coin; final bus = globalEventBusForTesting ?? GlobalEventBus.instance; @@ -161,10 +159,12 @@ class ElectrumXClient { // setting to null should force the creation of a new json rpc client // on the next request sent through this electrumx instance _electrumAdapterChannel = null; - _electrumAdapterClient = null; + await (await ClientManager.sharedInstance + .remove(cryptoCurrency: cryptoCurrency)) + ?.close(); // Also close any chain height services that are currently open. - await ChainHeightServiceManager.dispose(); + // await ChainHeightServiceManager.dispose(); }, ); } @@ -173,7 +173,7 @@ class ElectrumXClient { required ElectrumXNode node, required Prefs prefs, required List failovers, - required Coin coin, + required CryptoCurrency cryptoCurrency, TorService? torService, EventBus? globalEventBusForTesting, }) { @@ -185,7 +185,7 @@ class ElectrumXClient { torService: torService, failovers: failovers, globalEventBusForTesting: globalEventBusForTesting, - coin: coin, + cryptoCurrency: cryptoCurrency, ); } @@ -197,7 +197,11 @@ class ElectrumXClient { return true; } - Future checkElectrumAdapter() async { + Future closeAdapter() async { + await getElectrumAdapter()?.close(); + } + + Future _checkElectrumAdapter() async { ({InternetAddress host, int port})? proxyInfo; // If we're supposed to use Tor... @@ -206,15 +210,19 @@ class ElectrumXClient { if (_torService.status != TorConnectionStatus.connected) { // And the killswitch isn't set... if (!_prefs.torKillSwitch) { - // Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function. + // Then we'll just proceed and connect to ElectrumX through + // clearnet at the bottom of this function. Logging.instance.log( - "Tor preference set but Tor is not enabled, killswitch not set, connecting to Electrum adapter through clearnet", + "Tor preference set but Tor is not enabled, killswitch not set," + " connecting to Electrum adapter through clearnet", level: LogLevel.Warning, ); } else { // ... But if the killswitch is set, then we throw an exception. throw Exception( - "Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter"); + "Tor preference and killswitch set but Tor is not enabled, " + "not connecting to Electrum adapter", + ); // TODO [prio=low]: Try to start Tor. } } else { @@ -223,75 +231,60 @@ class ElectrumXClient { } } - // TODO [prio=med]: Add proxyInfo to StreamChannel (or add to wrapper). - // if (_electrumAdapter!.proxyInfo != proxyInfo) { - // _electrumAdapter!.proxyInfo = proxyInfo; - // _electrumAdapter!.disconnect( - // reason: "Tor proxyInfo does not match current info", - // ); - // } - // If the current ElectrumAdapterClient is closed, create a new one. - if (_electrumAdapterClient != null && - _electrumAdapterClient!.peer.isClosed) { + if (getElectrumAdapter() != null && getElectrumAdapter()!.peer.isClosed) { _electrumAdapterChannel = null; - _electrumAdapterClient = null; + await ClientManager.sharedInstance.remove(cryptoCurrency: cryptoCurrency); } + final String useHost; + final int usePort; + final bool useUseSSL; + if (currentFailoverIndex == -1) { - _electrumAdapterChannel ??= await electrum_adapter.connect( - host, - port: port, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients, - acceptUnverified: true, - useSSL: useSSL, - proxyInfo: proxyInfo, - ); - if (_coin == Coin.firo || _coin == Coin.firoTestNet) { - _electrumAdapterClient ??= FiroElectrumClient( - _electrumAdapterChannel!, - host, - port, - useSSL, - proxyInfo, - ); - } else { - _electrumAdapterClient ??= ElectrumClient( - _electrumAdapterChannel!, - host, - port, - useSSL, - proxyInfo, - ); - } + useHost = host; + usePort = port; + useUseSSL = useSSL; } else { - _electrumAdapterChannel ??= await electrum_adapter.connect( - failovers![currentFailoverIndex].address, - port: failovers![currentFailoverIndex].port, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients, - acceptUnverified: true, - useSSL: failovers![currentFailoverIndex].useSSL, - proxyInfo: proxyInfo, - ); - if (_coin == Coin.firo || _coin == Coin.firoTestNet) { - _electrumAdapterClient ??= FiroElectrumClient( + useHost = failovers![currentFailoverIndex].address; + usePort = failovers![currentFailoverIndex].port; + useUseSSL = failovers![currentFailoverIndex].useSSL; + } + + _electrumAdapterChannel ??= await electrum_adapter.connect( + useHost, + port: usePort, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients, + acceptUnverified: true, + useSSL: useUseSSL, + proxyInfo: proxyInfo, + ); + + if (getElectrumAdapter() == null) { + final ElectrumClient newClient; + if (cryptoCurrency is Firo) { + newClient = FiroElectrumClient( _electrumAdapterChannel!, - failovers![currentFailoverIndex].address, - failovers![currentFailoverIndex].port, - failovers![currentFailoverIndex].useSSL, + useHost, + usePort, + useUseSSL, proxyInfo, ); } else { - _electrumAdapterClient ??= ElectrumClient( + newClient = ElectrumClient( _electrumAdapterChannel!, - failovers![currentFailoverIndex].address, - failovers![currentFailoverIndex].port, - failovers![currentFailoverIndex].useSSL, + useHost, + usePort, + useUseSSL, proxyInfo, ); } + + ClientManager.sharedInstance.addClient( + newClient, + cryptoCurrency: cryptoCurrency, + ); } return; @@ -311,13 +304,13 @@ class ElectrumXClient { if (_requireMutex) { await _torConnectingLock - .protect(() async => await checkElectrumAdapter()); + .protect(() async => await _checkElectrumAdapter()); } else { - await checkElectrumAdapter(); + await _checkElectrumAdapter(); } try { - final response = await _electrumAdapterClient!.request( + final response = await getElectrumAdapter()!.request( command, args, ); @@ -397,16 +390,16 @@ class ElectrumXClient { if (_requireMutex) { await _torConnectingLock - .protect(() async => await checkElectrumAdapter()); + .protect(() async => await _checkElectrumAdapter()); } else { - await checkElectrumAdapter(); + await _checkElectrumAdapter(); } try { - var futures = >[]; - _electrumAdapterClient!.peer.withBatch(() { + final futures = >[]; + getElectrumAdapter()!.peer.withBatch(() { for (final arg in args) { - futures.add(_electrumAdapterClient!.request(command, arg)); + futures.add(getElectrumAdapter()!.request(command, arg)); } }); final response = await Future.wait(futures); @@ -776,12 +769,16 @@ class ElectrumXClient { bool verbose = true, String? requestID, }) async { - Logging.instance.log("attempting to fetch blockchain.transaction.get...", - level: LogLevel.Info); - await checkElectrumAdapter(); - dynamic response = await _electrumAdapterClient!.getTransaction(txHash); - Logging.instance.log("Fetching blockchain.transaction.get finished", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch blockchain.transaction.get...", + level: LogLevel.Info, + ); + await _checkElectrumAdapter(); + final dynamic response = await getElectrumAdapter()!.getTransaction(txHash); + Logging.instance.log( + "Fetching blockchain.transaction.get finished", + level: LogLevel.Info, + ); if (!verbose) { return {"rawtx": response as String}; @@ -809,14 +806,18 @@ class ElectrumXClient { String blockhash = "", String? requestID, }) async { - Logging.instance.log("attempting to fetch lelantus.getanonymityset...", - level: LogLevel.Info); - await checkElectrumAdapter(); - Map response = - await (_electrumAdapterClient as FiroElectrumClient)! + Logging.instance.log( + "attempting to fetch lelantus.getanonymityset...", + level: LogLevel.Info, + ); + await _checkElectrumAdapter(); + final Map response = + await (getElectrumAdapter() as FiroElectrumClient) .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); - Logging.instance.log("Fetching lelantus.getanonymityset finished", - level: LogLevel.Info); + Logging.instance.log( + "Fetching lelantus.getanonymityset finished", + level: LogLevel.Info, + ); return response; } @@ -828,13 +829,17 @@ class ElectrumXClient { dynamic mints, String? requestID, }) async { - Logging.instance.log("attempting to fetch lelantus.getmintmetadata...", - level: LogLevel.Info); - await checkElectrumAdapter(); - dynamic response = await (_electrumAdapterClient as FiroElectrumClient)! + Logging.instance.log( + "attempting to fetch lelantus.getmintmetadata...", + level: LogLevel.Info, + ); + await _checkElectrumAdapter(); + final dynamic response = await (getElectrumAdapter() as FiroElectrumClient) .getLelantusMintData(mints: mints); - Logging.instance.log("Fetching lelantus.getmintmetadata finished", - level: LogLevel.Info); + Logging.instance.log( + "Fetching lelantus.getmintmetadata finished", + level: LogLevel.Info, + ); return response; } @@ -844,19 +849,23 @@ class ElectrumXClient { String? requestID, required int startNumber, }) async { - Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...", - level: LogLevel.Info); - await checkElectrumAdapter(); + Logging.instance.log( + "attempting to fetch lelantus.getusedcoinserials...", + level: LogLevel.Info, + ); + await _checkElectrumAdapter(); int retryCount = 3; dynamic response; while (retryCount > 0 && response is! List) { - response = await (_electrumAdapterClient as FiroElectrumClient)! + response = await (getElectrumAdapter() as FiroElectrumClient) .getLelantusUsedCoinSerials(startNumber: startNumber); // TODO add 2 minute timeout. - Logging.instance.log("Fetching lelantus.getusedcoinserials finished", - level: LogLevel.Info); + Logging.instance.log( + "Fetching lelantus.getusedcoinserials finished", + level: LogLevel.Info, + ); retryCount--; } @@ -868,13 +877,17 @@ class ElectrumXClient { /// /// ex: 1 Future getLelantusLatestCoinId({String? requestID}) async { - Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...", - level: LogLevel.Info); - await checkElectrumAdapter(); - int response = - await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId(); - Logging.instance.log("Fetching lelantus.getlatestcoinid finished", - level: LogLevel.Info); + Logging.instance.log( + "attempting to fetch lelantus.getlatestcoinid...", + level: LogLevel.Info, + ); + await _checkElectrumAdapter(); + final int response = + await (getElectrumAdapter() as FiroElectrumClient).getLatestCoinId(); + Logging.instance.log( + "Fetching lelantus.getlatestcoinid finished", + level: LogLevel.Info, + ); return response; } @@ -899,15 +912,21 @@ class ElectrumXClient { String? requestID, }) async { try { - Logging.instance.log("attempting to fetch spark.getsparkanonymityset...", - level: LogLevel.Info); - await checkElectrumAdapter(); - Map response = - await (_electrumAdapterClient as FiroElectrumClient) + Logging.instance.log( + "attempting to fetch spark.getsparkanonymityset...", + level: LogLevel.Info, + ); + await _checkElectrumAdapter(); + final Map response = + await (getElectrumAdapter() as FiroElectrumClient) .getSparkAnonymitySet( - coinGroupId: coinGroupId, startBlockHash: startBlockHash); - Logging.instance.log("Fetching spark.getsparkanonymityset finished", - level: LogLevel.Info); + coinGroupId: coinGroupId, + startBlockHash: startBlockHash, + ); + Logging.instance.log( + "Fetching spark.getsparkanonymityset finished", + level: LogLevel.Info, + ); return response; } catch (e) { rethrow; @@ -922,15 +941,20 @@ class ElectrumXClient { }) async { try { // Use electrum_adapter package's getSparkUsedCoinsTags method. - Logging.instance.log("attempting to fetch spark.getusedcoinstags...", - level: LogLevel.Info); - await checkElectrumAdapter(); - Map response = - await (_electrumAdapterClient as FiroElectrumClient) + Logging.instance.log( + "attempting to fetch spark.getusedcoinstags...", + level: LogLevel.Info, + ); + await _checkElectrumAdapter(); + final Map response = + await (getElectrumAdapter() as FiroElectrumClient) .getUsedCoinsTags(startNumber: startNumber); // TODO: Add 2 minute timeout. - Logging.instance.log("Fetching spark.getusedcoinstags finished", - level: LogLevel.Info); + // Why 2 minutes? + Logging.instance.log( + "Fetching spark.getusedcoinstags finished", + level: LogLevel.Info, + ); final map = Map.from(response); final set = Set.from(map["tags"] as List); return await compute(_ffiHashTagsComputeWrapper, set); @@ -955,14 +979,18 @@ class ElectrumXClient { required List sparkCoinHashes, }) async { try { - Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...", - level: LogLevel.Info); - await checkElectrumAdapter(); - List response = - await (_electrumAdapterClient as FiroElectrumClient) + Logging.instance.log( + "attempting to fetch spark.getsparkmintmetadata...", + level: LogLevel.Info, + ); + await _checkElectrumAdapter(); + final List response = + await (getElectrumAdapter() as FiroElectrumClient) .getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes); - Logging.instance.log("Fetching spark.getsparkmintmetadata finished", - level: LogLevel.Info); + Logging.instance.log( + "Fetching spark.getsparkmintmetadata finished", + level: LogLevel.Info, + ); return List>.from(response); } catch (e) { Logging.instance.log(e, level: LogLevel.Error); @@ -977,13 +1005,17 @@ class ElectrumXClient { String? requestID, }) async { try { - Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...", - level: LogLevel.Info); - await checkElectrumAdapter(); - int response = await (_electrumAdapterClient as FiroElectrumClient) + Logging.instance.log( + "attempting to fetch spark.getsparklatestcoinid...", + level: LogLevel.Info, + ); + await _checkElectrumAdapter(); + final int response = await (getElectrumAdapter() as FiroElectrumClient) .getSparkLatestCoinId(); - Logging.instance.log("Fetching spark.getsparklatestcoinid finished", - level: LogLevel.Info); + Logging.instance.log( + "Fetching spark.getsparklatestcoinid finished", + level: LogLevel.Info, + ); return response; } catch (e) { Logging.instance.log(e, level: LogLevel.Error); @@ -1001,11 +1033,12 @@ class ElectrumXClient { /// "rate": 1000, /// } Future> getFeeRate({String? requestID}) async { - await checkElectrumAdapter(); - return await _electrumAdapterClient!.getFeeRate(); + await _checkElectrumAdapter(); + return await getElectrumAdapter()!.getFeeRate(); } - /// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks]. + /// Return the estimated transaction fee per kilobyte for a transaction to be + /// confirmed within a certain number of [blocks]. /// /// Returns a Decimal fee rate /// Ex: @@ -1022,7 +1055,7 @@ class ElectrumXClient { try { // If the response is -1 or null, return a temporary hardcoded value for // Dogecoin. This is a temporary fix until the fee estimation is fixed. - if (coin == Coin.dogecoin && + if (cryptoCurrency is Dogecoin && (response == null || response == -1 || Decimal.parse(response.toString()) == Decimal.parse("-1"))) { @@ -1035,7 +1068,7 @@ class ElectrumXClient { return Decimal.parse(response.toString()); } catch (e, s) { final String msg = "Error parsing fee rate. Response: $response" - "\nResult: ${response}\nError: $e\nStack trace: $s"; + "\nResult: $response\nError: $e\nStack trace: $s"; Logging.instance.log(msg, level: LogLevel.Fatal); throw Exception(msg); } diff --git a/lib/electrumx_rpc/rpc.dart b/lib/electrumx_rpc/rpc.dart deleted file mode 100644 index f2044a141..000000000 --- a/lib/electrumx_rpc/rpc.dart +++ /dev/null @@ -1,413 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:mutex/mutex.dart'; -import 'package:stackwallet/exceptions/json_rpc/json_rpc_exception.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; -import 'package:tor_ffi_plugin/socks_socket.dart'; - -// Json RPC class to handle connecting to electrumx servers -class JsonRPC { - JsonRPC({ - required this.host, - required this.port, - this.useSSL = false, - this.connectionTimeout = const Duration(seconds: 60), - required ({InternetAddress host, int port})? proxyInfo, - }); - final bool useSSL; - final String host; - final int port; - final Duration connectionTimeout; - ({InternetAddress host, int port})? proxyInfo; - - final _requestMutex = Mutex(); - final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue(); - Socket? _socket; - SOCKSSocket? _socksSocket; - StreamSubscription>? _subscription; - - void _dataHandler(List data) { - _requestQueue.nextIncompleteReq.then((req) { - if (req != null) { - req.appendDataAndCheckIfComplete(data); - - if (req.isComplete) { - _onReqCompleted(req); - } - } else { - Logging.instance.log( - "_dataHandler found a null req!", - level: LogLevel.Warning, - ); - } - }); - } - - void _errorHandler(Object error, StackTrace trace) { - _requestQueue.nextIncompleteReq.then((req) { - if (req != null) { - req.completer.completeError(error, trace); - _onReqCompleted(req); - } - }); - } - - void _doneHandler() { - disconnect(reason: "JsonRPC _doneHandler() called"); - } - - void _onReqCompleted(_JsonRPCRequest req) { - _requestQueue.remove(req).then((_) { - // attempt to send next request - _sendNextAvailableRequest(); - }); - } - - void _sendNextAvailableRequest() { - _requestQueue.nextIncompleteReq.then((req) { - if (req != null) { - if (!Prefs.instance.useTor) { - if (_socket == null) { - Logging.instance.log( - "JsonRPC _sendNextAvailableRequest attempted with" - " _socket=null on $host:$port", - level: LogLevel.Error, - ); - } - // \r\n required by electrumx server - _socket!.write('${req.jsonRequest}\r\n'); - } else { - if (_socksSocket == null) { - Logging.instance.log( - "JsonRPC _sendNextAvailableRequest attempted with" - " _socksSocket=null on $host:$port", - level: LogLevel.Error, - ); - } - // \r\n required by electrumx server - _socksSocket?.write('${req.jsonRequest}\r\n'); - } - - // TODO different timeout length? - req.initiateTimeout( - onTimedOut: () { - _onReqCompleted(req); - }, - ); - } - }); - } - - Future request( - String jsonRpcRequest, - Duration requestTimeout, - ) async { - await _requestMutex.protect(() async { - if (!Prefs.instance.useTor) { - if (_socket == null) { - Logging.instance.log( - "JsonRPC request: opening socket $host:$port", - level: LogLevel.Info, - ); - await _connect().timeout(requestTimeout, onTimeout: () { - throw Exception("Request timeout: $jsonRpcRequest"); - }); - } - } else { - if (_socksSocket == null) { - Logging.instance.log( - "JsonRPC request: opening SOCKS socket to $host:$port", - level: LogLevel.Info, - ); - await _connect().timeout(requestTimeout, onTimeout: () { - throw Exception("Request timeout: $jsonRpcRequest"); - }); - } - } - }); - - final req = _JsonRPCRequest( - jsonRequest: jsonRpcRequest, - requestTimeout: requestTimeout, - completer: Completer(), - ); - - final future = req.completer.future.onError( - (error, stackTrace) async { - await disconnect( - reason: "return req.completer.future.onError: $error\n$stackTrace", - ); - return JsonRPCResponse( - exception: error is JsonRpcException - ? error - : JsonRpcException( - "req.completer.future.onError: $error\n$stackTrace", - ), - ); - }, - ); - - // if this is the only/first request then send it right away - await _requestQueue.add( - req, - onInitialRequestAdded: _sendNextAvailableRequest, - ); - - return future; - } - - /// DO NOT set [ignoreMutex] to true unless fully aware of the consequences - Future disconnect({ - required String reason, - bool ignoreMutex = false, - }) async { - if (ignoreMutex) { - await _disconnectHelper(reason: reason); - } else { - await _requestMutex.protect(() async { - await _disconnectHelper(reason: reason); - }); - } - } - - Future _disconnectHelper({required String reason}) async { - await _subscription?.cancel(); - _subscription = null; - _socket?.destroy(); - _socket = null; - await _socksSocket?.close(); - _socksSocket = null; - - // clean up remaining queue - await _requestQueue.completeRemainingWithError( - "JsonRPC disconnect() called with reason: \"$reason\"", - ); - } - - Future _connect() async { - // ignore mutex is set to true here as _connect is already called within - // the mutex.protect block. Setting to false here leads to a deadlock - await disconnect( - reason: "New connection requested", - ignoreMutex: true, - ); - - if (!Prefs.instance.useTor) { - if (useSSL) { - _socket = await SecureSocket.connect( - host, - port, - timeout: connectionTimeout, - onBadCertificate: (_) => true, - ); // TODO do not automatically trust bad certificates. - } else { - _socket = await Socket.connect( - host, - port, - timeout: connectionTimeout, - ); - } - - _subscription = _socket!.listen( - _dataHandler, - onError: _errorHandler, - onDone: _doneHandler, - cancelOnError: true, - ); - } else { - if (proxyInfo == null) { - throw JsonRpcException( - "JsonRPC.connect failed with useTor=${Prefs.instance.useTor} and proxyInfo is null"); - } - - // instantiate a socks socket at localhost and on the port selected by the tor service - _socksSocket = await SOCKSSocket.create( - proxyHost: proxyInfo!.host.address, - proxyPort: proxyInfo!.port, - sslEnabled: useSSL, - ); - - try { - Logging.instance.log( - "JsonRPC.connect(): connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...", - level: LogLevel.Info); - - await _socksSocket?.connect(); - - Logging.instance.log( - "JsonRPC.connect(): connected to SOCKS socket at $proxyInfo...", - level: LogLevel.Info); - } catch (e) { - Logging.instance.log( - "JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e", - level: LogLevel.Error); - throw JsonRpcException( - "JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e"); - } - - try { - Logging.instance.log( - "JsonRPC.connect(): connecting to $host:$port over SOCKS socket at $proxyInfo...", - level: LogLevel.Info); - - await _socksSocket?.connectTo(host, port); - - Logging.instance.log( - "JsonRPC.connect(): connected to $host:$port over SOCKS socket at $proxyInfo", - level: LogLevel.Info); - } catch (e) { - Logging.instance.log( - "JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo, $e", - level: LogLevel.Error); - throw JsonRpcException( - "JsonRPC.connect(): failed to connect to tor proxy, $e"); - } - - _subscription = _socksSocket!.listen( - _dataHandler, - onError: _errorHandler, - onDone: _doneHandler, - cancelOnError: true, - ); - } - - return; - } -} - -class _JsonRPCRequestQueue { - final _lock = Mutex(); - final List<_JsonRPCRequest> _rq = []; - - Future add( - _JsonRPCRequest req, { - VoidCallback? onInitialRequestAdded, - }) async { - return await _lock.protect(() async { - _rq.add(req); - if (_rq.length == 1) { - onInitialRequestAdded?.call(); - } - }); - } - - Future remove(_JsonRPCRequest req) async { - return await _lock.protect(() async { - final result = _rq.remove(req); - return result; - }); - } - - Future<_JsonRPCRequest?> get nextIncompleteReq async { - return await _lock.protect(() async { - int removeCount = 0; - _JsonRPCRequest? returnValue; - for (final req in _rq) { - if (req.isComplete) { - removeCount++; - } else { - returnValue = req; - break; - } - } - - _rq.removeRange(0, removeCount); - - return returnValue; - }); - } - - Future completeRemainingWithError( - String error, { - StackTrace? stackTrace, - }) async { - await _lock.protect(() async { - for (final req in _rq) { - if (!req.isComplete) { - req.completer.completeError(Exception(error), stackTrace); - } - } - _rq.clear(); - }); - } - - Future get isEmpty async { - return await _lock.protect(() async { - return _rq.isEmpty; - }); - } -} - -class _JsonRPCRequest { - // 0x0A is newline - // https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html - static const int separatorByte = 0x0A; - - final String jsonRequest; - final Completer completer; - final Duration requestTimeout; - final List _responseData = []; - - _JsonRPCRequest({ - required this.jsonRequest, - required this.completer, - required this.requestTimeout, - }); - - void appendDataAndCheckIfComplete(List data) { - _responseData.addAll(data); - if (data.last == separatorByte) { - try { - final response = json.decode(String.fromCharCodes(_responseData)); - completer.complete(JsonRPCResponse(data: response)); - } catch (e, s) { - Logging.instance.log( - "JsonRPC json.decode: $e\n$s", - level: LogLevel.Error, - ); - completer.completeError(e, s); - } - } - } - - void initiateTimeout({ - required VoidCallback onTimedOut, - }) { - Future.delayed(requestTimeout).then((_) { - if (!isComplete) { - completer.complete( - JsonRPCResponse( - data: null, - exception: JsonRpcException( - "_JsonRPCRequest timed out: $jsonRequest", - ), - ), - ); - } - onTimedOut.call(); - }); - } - - bool get isComplete => completer.isCompleted; -} - -class JsonRPCResponse { - final dynamic data; - final JsonRpcException? exception; - - JsonRPCResponse({this.data, this.exception}); -} diff --git a/lib/frost_route_generator.dart b/lib/frost_route_generator.dart new file mode 100644 index 000000000..d401c7030 --- /dev/null +++ b/lib/frost_route_generator.dart @@ -0,0 +1,275 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; + +typedef FrostStepRoute = ({String routeName, String title}); + +enum FrostInterruptionDialogType { + walletCreation, + resharing, + transactionCreation; +} + +final pFrostCreateCurrentStep = StateProvider.autoDispose((ref) => 1); +final pFrostScaffoldCanPopDesktop = StateProvider.autoDispose((_) => false); +final pFrostScaffoldArgs = StateProvider< + ({ + ({String walletName, FrostCurrency frostCurrency}) info, + String? walletId, + List stepRoutes, + FrostInterruptionDialogType frostInterruptionDialogType, + NavigatorState parentNav, + String callerRouteName, + })?>((ref) => null); + +abstract class FrostRouteGenerator { + static const bool useMaterialPageRoute = true; + + static const List createNewConfigStepRoutes = [ + (routeName: FrostCreateStep1a.routeName, title: FrostCreateStep1a.title), + (routeName: FrostCreateStep2.routeName, title: FrostCreateStep2.title), + (routeName: FrostCreateStep3.routeName, title: FrostCreateStep3.title), + (routeName: FrostCreateStep4.routeName, title: FrostCreateStep4.title), + (routeName: FrostCreateStep5.routeName, title: FrostCreateStep5.title), + ]; + + static const List importNewConfigStepRoutes = [ + (routeName: FrostCreateStep1b.routeName, title: FrostCreateStep1b.title), + (routeName: FrostCreateStep2.routeName, title: FrostCreateStep2.title), + (routeName: FrostCreateStep3.routeName, title: FrostCreateStep3.title), + (routeName: FrostCreateStep4.routeName, title: FrostCreateStep4.title), + (routeName: FrostCreateStep5.routeName, title: FrostCreateStep5.title), + ]; + + static const List initiateReshareStepRoutes = [ + (routeName: FrostReshareStep1a.routeName, title: FrostReshareStep1a.title), + ( + routeName: FrostReshareStep2abd.routeName, + title: FrostReshareStep2abd.title + ), + ( + routeName: FrostReshareStep3abd.routeName, + title: FrostReshareStep3abd.title + ), + (routeName: FrostReshareStep4.routeName, title: FrostReshareStep4.title), + (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), + ]; + + static const List importReshareStepRoutes = [ + (routeName: FrostReshareStep1b.routeName, title: FrostReshareStep1b.title), + ( + routeName: FrostReshareStep2abd.routeName, + title: FrostReshareStep2abd.title + ), + ( + routeName: FrostReshareStep3abd.routeName, + title: FrostReshareStep3abd.title + ), + (routeName: FrostReshareStep4.routeName, title: FrostReshareStep4.title), + (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), + ]; + + static const List joinReshareStepRoutes = [ + (routeName: FrostReshareStep1c.routeName, title: FrostReshareStep1c.title), + (routeName: FrostReshareStep2c.routeName, title: FrostReshareStep2c.title), + (routeName: FrostReshareStep3c.routeName, title: FrostReshareStep3c.title), + (routeName: FrostReshareStep4.routeName, title: FrostReshareStep4.title), + (routeName: FrostReshareStep5.routeName, title: FrostReshareStep5.title), + ]; + + static const List sendFrostTxStepRoutes = [ + (routeName: FrostSendStep1a.routeName, title: FrostSendStep1a.title), + (routeName: FrostSendStep2.routeName, title: FrostSendStep2.title), + (routeName: FrostSendStep3.routeName, title: FrostSendStep3.title), + (routeName: FrostSendStep4.routeName, title: FrostSendStep4.title), + ]; + + static const List signFrostTxStepRoutes = [ + (routeName: FrostSendStep1b.routeName, title: FrostSendStep1b.title), + (routeName: FrostSendStep2.routeName, title: FrostSendStep2.title), + (routeName: FrostSendStep3.routeName, title: FrostSendStep3.title), + (routeName: FrostSendStep4.routeName, title: FrostSendStep4.title), + ]; + + static Route generateRoute(RouteSettings settings) { + final args = settings.arguments; + + switch (settings.name) { + case FrostCreateStep1a.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep1a(), + settings: settings, + ); + + case FrostCreateStep1b.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep1b(), + settings: settings, + ); + + case FrostCreateStep2.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep2(), + settings: settings, + ); + + case FrostCreateStep3.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep3(), + settings: settings, + ); + + case FrostCreateStep4.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep4(), + settings: settings, + ); + + case FrostCreateStep5.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostCreateStep5(), + settings: settings, + ); + + case FrostReshareStep1a.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep1a(), + settings: settings, + ); + + case FrostReshareStep1b.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep1b(), + settings: settings, + ); + + case FrostReshareStep1c.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep1c(), + settings: settings, + ); + + case FrostReshareStep2abd.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep2abd(), + settings: settings, + ); + + case FrostReshareStep2c.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep2c(), + settings: settings, + ); + + case FrostReshareStep3abd.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep3abd(), + settings: settings, + ); + + case FrostReshareStep3c.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep3c(), + settings: settings, + ); + + case FrostReshareStep4.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep4(), + settings: settings, + ); + + case FrostReshareStep5.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostReshareStep5(), + settings: settings, + ); + + case FrostSendStep1a.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep1a(), + settings: settings, + ); + + case FrostSendStep1b.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep1b(), + settings: settings, + ); + + case FrostSendStep2.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep2(), + settings: settings, + ); + + case FrostSendStep3.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep3(), + settings: settings, + ); + + case FrostSendStep4.routeName: + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostSendStep4(), + settings: settings, + ); + + default: + return _routeError(""); + } + } + + static Route _routeError(String message) { + return RouteGenerator.getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => Placeholder( + child: Center( + child: Text(message), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 54ccf3866..011b8a27a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -687,6 +687,7 @@ class _MaterialAppWithThemeState extends ConsumerState appBarTheme: AppBarTheme( centerTitle: false, color: colorScheme.background, + surfaceTintColor: colorScheme.background, elevation: 0, ), inputDecorationTheme: InputDecorationTheme( diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index e3368a119..e3314d754 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -163,16 +163,18 @@ enum AddressType { spark, stellar, tezos, - ; + frostMS, + p2tr, + solana; String get readableName { switch (this) { case AddressType.p2pkh: - return "Legacy"; + return "P2PKH"; case AddressType.p2sh: return "Wrapped segwit"; case AddressType.p2wpkh: - return "Segwit"; + return "P2WPKH (segwit)"; case AddressType.cryptonote: return "Cryptonote"; case AddressType.mimbleWimble: @@ -193,6 +195,12 @@ enum AddressType { return "Stellar"; case AddressType.tezos: return "Tezos"; + case AddressType.frostMS: + return "FrostMS"; + case AddressType.solana: + return "Solana"; + case AddressType.p2tr: + return "P2TR (taproot)"; } } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index 796c29f29..7e78cbee8 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -266,6 +266,9 @@ const _AddresstypeEnumValueMap = { 'spark': 10, 'stellar': 11, 'tezos': 12, + 'frostMS': 13, + 'p2tr': 14, + 'solana': 15, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -281,6 +284,9 @@ const _AddresstypeValueEnumMap = { 10: AddressType.spark, 11: AddressType.stellar, 12: AddressType.tezos, + 13: AddressType.frostMS, + 14: AddressType.p2tr, + 15: AddressType.solana, }; Id _addressGetId(Address object) { diff --git a/lib/models/signing_data.dart b/lib/models/signing_data.dart index 0937a2d8b..24dac4546 100644 --- a/lib/models/signing_data.dart +++ b/lib/models/signing_data.dart @@ -8,9 +8,7 @@ * */ -import 'dart:typed_data'; - -import 'package:bitcoindart/bitcoindart.dart'; +import 'package:coinlib_flutter/coinlib_flutter.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; @@ -18,25 +16,19 @@ class SigningData { SigningData({ required this.derivePathType, required this.utxo, - this.output, this.keyPair, - this.redeemScript, }); final DerivePathType derivePathType; final UTXO utxo; - Uint8List? output; - ECPair? keyPair; - Uint8List? redeemScript; + HDPrivateKey? keyPair; @override String toString() { return "SigningData{\n" " derivePathType: $derivePathType,\n" " utxo: $utxo,\n" - " output: $output,\n" " keyPair: $keyPair,\n" - " redeemScript: $redeemScript,\n" "}"; } } diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 32e1b618c..34048fcbb 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -11,6 +11,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -46,7 +47,7 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; class AddWalletView extends ConsumerStatefulWidget { - const AddWalletView({Key? key}) : super(key: key); + const AddWalletView({super.key}); static const routeName = "/addWallet"; @@ -134,6 +135,11 @@ class _AddWalletViewState extends ConsumerState { _coins.remove(Coin.wownero); } + if (Util.isDesktop && !kDebugMode) { + _coins.remove(Coin.bitcoinFrost); + _coins.remove(Coin.bitcoinFrostTestNet); + } + coinEntities.addAll(_coins.map((e) => CoinEntity(e))); if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) { diff --git a/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart new file mode 100644 index 000000000..5ce23eaad --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart @@ -0,0 +1,447 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/frost_mascot.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class CreateNewFrostMsWalletView extends ConsumerStatefulWidget { + const CreateNewFrostMsWalletView({ + super.key, + required this.walletName, + required this.frostCurrency, + }); + + static const String routeName = "/createNewFrostMsWalletView"; + + final String walletName; + final FrostCurrency frostCurrency; + + @override + ConsumerState createState() => + _NewFrostMsWalletViewState(); +} + +class _NewFrostMsWalletViewState + extends ConsumerState { + final _thresholdController = TextEditingController(); + final _participantsController = TextEditingController(); + + final List controllers = []; + + int _participantsCount = 0; + + String _validateInputData() { + final threshold = int.tryParse(_thresholdController.text); + if (threshold == null) { + return "Choose a threshold"; + } + + final partsCount = int.tryParse(_participantsController.text); + if (partsCount == null) { + return "Choose total number of participants"; + } + + if (threshold > partsCount) { + return "Threshold cannot be greater than the number of participants"; + } + + if (partsCount < 2) { + return "At least two participants required"; + } + + if (controllers.length != partsCount) { + return "Participants count error"; + } + + final hasEmptyParticipants = controllers + .map((e) => e.text.trim().isEmpty) + .reduce((value, element) => value |= element); + if (hasEmptyParticipants) { + return "Participants must not be empty"; + } + + if (controllers.length != + controllers.map((e) => e.text.trim()).toSet().length) { + return "Duplicate participant name found"; + } + + return "valid"; + } + + void _participantsCountChanged(String newValue) { + final count = int.tryParse(newValue); + if (count != null) { + if (count > _participantsCount) { + for (int i = _participantsCount; i < count; i++) { + controllers.add(TextEditingController()); + } + + _participantsCount = count; + setState(() {}); + } else if (count < _participantsCount) { + for (int i = _participantsCount; i > count; i--) { + final last = controllers.removeLast(); + last.dispose(); + } + + _participantsCount = count; + setState(() {}); + } + } + } + + void _showWhatIsThresholdDialog() { + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "What is a threshold?", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + Text( + "A threshold is the amount of people required to perform an " + "action. This does not have to be the same number as the " + "total number in the group.", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 6, + ), + Text( + "For example, if you have 3 people in the group, but a threshold " + "of 2, then you only need 2 out of the 3 people to sign for an " + "action to take place.", + style: STextStyles.w400_16(context), + ), + const SizedBox( + height: 6, + ), + Text( + "Conversely if you have a group of 3 AND a threshold of 3, you " + "will need all 3 people in the group to sign to approve any " + "action.", + style: STextStyles.w400_16(context), + ), + ], + ), + ), + ); + } + + @override + void dispose() { + _thresholdController.dispose(); + _participantsController.dispose(); + for (final e in controllers) { + e.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + // TODO: [prio=high] get rid of placeholder text?? + trailing: FrostMascot( + title: 'Lorem ipsum', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Create new group", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Threshold", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + CustomTextButton( + text: "What is a threshold?", + onTap: _showWhatIsThresholdDialog, + ), + ], + ), + const SizedBox( + height: 10, + ), + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _thresholdController, + decoration: InputDecoration( + hintText: "Enter number of signatures", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 16, + ), + Text( + "Number of participants", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + const SizedBox( + height: 10, + ), + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _participantsController, + onChanged: _participantsCountChanged, + decoration: InputDecoration( + hintText: "Enter number of participants", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + child: Text( + "Enter number of signatures required for fund management", + style: STextStyles.label(context), + ), + ), + ), + ], + ), + ], + ), + const SizedBox( + height: 16, + ), + if (controllers.isNotEmpty) + Text( + "My name", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + if (controllers.isNotEmpty) + const SizedBox( + height: 10, + ), + if (controllers.isNotEmpty) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: controllers.first, + decoration: InputDecoration( + hintText: "Enter your name", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + child: Text( + "Type your name in one word without spaces", + style: STextStyles.label(context), + ), + ), + ), + ], + ), + ], + ), + if (controllers.length > 1) + const SizedBox( + height: 16, + ), + if (controllers.length > 1) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Remaining participants", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + child: Text( + "Type each name in one word without spaces", + style: STextStyles.label(context), + ), + ), + ), + ], + ), + ], + ), + if (controllers.length > 1) + Column( + children: [ + for (int i = 1; i < controllers.length; i++) + Padding( + padding: const EdgeInsets.only( + top: 10, + ), + child: TextField( + controller: controllers[i], + decoration: InputDecoration( + hintText: "Enter name", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Create new group", + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final validationMessage = _validateInputData(); + + if (validationMessage != "valid") { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: validationMessage, + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + final config = Frost.createMultisigConfig( + name: controllers.first.text.trim(), + threshold: int.parse(_thresholdController.text), + participants: controllers.map((e) => e.text.trim()).toList(), + ); + + ref.read(pFrostMyName.notifier).state = + controllers.first.text.trim(); + ref.read(pFrostMultisigConfig.notifier).state = config; + + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: widget.walletName, + frostCurrency: widget.frostCurrency, + ), + walletId: null, + stepRoutes: FrostRouteGenerator.createNewConfigStepRoutes, + frostInterruptionDialogType: + FrostInterruptionDialogType.walletCreation, + parentNav: Navigator.of(context), + callerRouteName: CreateNewFrostMsWalletView.routeName, + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart new file mode 100644 index 000000000..1a7378bc6 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart @@ -0,0 +1,320 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class SelectNewFrostImportTypeView extends ConsumerStatefulWidget { + const SelectNewFrostImportTypeView({ + super.key, + required this.walletName, + required this.frostCurrency, + }); + + static const String routeName = "/selectNewFrostImportTypeView"; + + final String walletName; + final FrostCurrency frostCurrency; + + @override + ConsumerState createState() => + _SelectNewFrostImportTypeViewState(); +} + +class _SelectNewFrostImportTypeViewState + extends ConsumerState { + _ImportOption _selectedOption = _ImportOption.multisigNew; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (content) => DesktopScaffold( + appBar: const DesktopAppBar( + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + isCompactHeight: false, + ), + body: SizedBox( + width: 480, + child: content, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (content) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + colorFilter: ColorFilter.mode( + Theme.of(context) + .extension()! + .topNavIconPrimary, + BlendMode.srcIn, + ), + ), + onPressed: () async { + await showDialog( + context: context, + builder: (_) => const _FrostJoinInfoDialog(), + ); + }, + ), + ), + ], + ), + body: Container( + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (ctx, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: content, + ), + ), + ); + }, + ), + ), + ), + ), + ), + child: Column( + children: [ + ..._ImportOption.values.map( + (e) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: _ImportOptionCard( + onPressed: () => setState(() => _selectedOption = e), + title: e.info, + description: e.description, + value: e, + groupValue: _selectedOption, + ), + ), + ), + const Spacer(), + PrimaryButton( + label: "Continue", + onPressed: () async { + switch (_selectedOption) { + case _ImportOption.multisigNew: + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: widget.walletName, + frostCurrency: widget.frostCurrency, + ), + walletId: null, // no wallet id yet + stepRoutes: FrostRouteGenerator.importNewConfigStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.walletCreation, + callerRouteName: SelectNewFrostImportTypeView.routeName, + ); + break; + + case _ImportOption.resharerExisting: + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: widget.walletName, + frostCurrency: widget.frostCurrency, + ), + walletId: null, // no wallet id yet + stepRoutes: FrostRouteGenerator.joinReshareStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.resharing, + callerRouteName: SelectNewFrostImportTypeView.routeName, + ); + break; + } + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + }, + ) + ], + ), + ), + ); + } +} + +enum _ImportOption { + multisigNew, + resharerExisting; + + String get info { + switch (this) { + case _ImportOption.multisigNew: + return "I want to join a new group"; + case _ImportOption.resharerExisting: + return "I want to join an existing group"; + } + } + + String get description { + switch (this) { + case _ImportOption.multisigNew: + return "You are currently participating in the process of creating a new group"; + case _ImportOption.resharerExisting: + return "You are joining an existing group through the process of resharing"; + } + } +} + +class _ImportOptionCard extends StatefulWidget { + const _ImportOptionCard({ + super.key, + required this.onPressed, + required this.title, + required this.description, + required this.value, + required this.groupValue, + }); + + final VoidCallback onPressed; + final String title; + final String description; + final _ImportOption value; + final _ImportOption groupValue; + + @override + State<_ImportOptionCard> createState() => _ImportOptionCardState(); +} + +class _ImportOptionCardState extends State<_ImportOptionCard> { + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + onPressed: widget.onPressed, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: Radio( + value: widget.value, + groupValue: widget.groupValue, + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + onChanged: (_) => widget.onPressed(), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + top: 12.0, + right: 12.0, + bottom: 12.0, + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + widget.title, + style: STextStyles.w600_16(context), + ), + ), + ], + ), + const SizedBox( + height: 2, + ), + Row( + children: [ + Expanded( + child: Text( + widget.description, + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _FrostJoinInfoDialog extends StatelessWidget { + const _FrostJoinInfoDialog({super.key}); + + @override + Widget build(BuildContext context) { + return SimpleMobileDialog( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Join a group", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + Text( + "You should select 'Join a new group' if you are creating a brand " + "new wallet with other people.", + style: STextStyles.w600_16(context), + ), + const SizedBox( + height: 12, + ), + Text( + "You should select 'Join an existing group' if you an existing " + "group is being edited and you are being added as a participant.", + style: STextStyles.w600_16(context), + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart new file mode 100644 index 000000000..0f1cd84aa --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1a.dart @@ -0,0 +1,248 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; + +class FrostCreateStep1a extends ConsumerStatefulWidget { + const FrostCreateStep1a({super.key}); + + static const String routeName = "/frostCreateStep1a"; + static const String title = "Multisig group info"; + + @override + ConsumerState createState() => _FrostCreateStep1aState(); +} + +class _FrostCreateStep1aState extends ConsumerState { + static const info = [ + "Share this config with the group participants.", + "Wait for them to join the group.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Generate keys”.", + ]; + + bool _userVerifyContinue = false; + + void _showParticipantsDialog() { + final participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + showCloseButton: false, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "Group participants", + style: STextStyles.w600_20(context), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "The names are case-sensitive and must be entered exactly.", + style: STextStyles.w400_16(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + ), + const SizedBox( + height: 12, + ), + for (final participant in participants) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + height: 1.5, + color: + Theme.of(context).extension()!.background, + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + participant, + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: participant, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + const SizedBox( + height: 24, + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 20, + ), + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostMultisigConfig.state).state ?? "Error", + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + DetailItem( + title: "Encoded config", + detail: ref.watch(pFrostMultisigConfig.state).state ?? "Error", + button: Util.isDesktop + ? IconCopyButton( + data: + ref.watch(pFrostMultisigConfig.state).state ?? "Error", + ) + : SimpleCopyButton( + data: + ref.watch(pFrostMultisigConfig.state).state ?? "Error", + ), + ), + SizedBox( + height: Util.isDesktop ? 64 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show group participants", + onPressed: _showParticipantsDialog, + ), + ), + ], + ), + if (!Util.isDesktop) + const Spacer( + flex: 2, + ), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has joined the group", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start key generation", + enabled: _userVerifyContinue, + onPressed: () async { + ref.read(pFrostStartKeyGenData.notifier).state = + Frost.startKeyGeneration( + multisigConfig: ref.watch(pFrostMultisigConfig.state).state!, + myName: ref.read(pFrostMyName.state).state!, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + // FrostShareCommitmentsView.routeName, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart new file mode 100644 index 000000000..21ed92a7d --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_1b.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostCreateStep1b extends ConsumerStatefulWidget { + const FrostCreateStep1b({super.key}); + + static const String routeName = "/frostCreateStep1b"; + static const String title = "Import group info"; + + @override + ConsumerState createState() => _FrostCreateStep1bState(); +} + +class _FrostCreateStep1bState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group creator.", + "Enter your name EXACTLY as the group creator entered it. When in doubt, " + "double check with them. The names are case-sensitive.", + "Wait for other participants to finish entering their information.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Generate keys”.", + ]; + + late final TextEditingController myNameFieldController, configFieldController; + late final FocusNode myNameFocusNode, configFocusNode; + + bool _nameEmpty = true, _configEmpty = true, _userVerifyContinue = false; + + @override + void initState() { + myNameFieldController = TextEditingController(); + configFieldController = TextEditingController(); + myNameFocusNode = FocusNode(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + myNameFieldController.dispose(); + configFieldController.dispose(); + myNameFocusNode.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 16, + ), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Enter config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 16, + ), + FrostStepField( + controller: myNameFieldController, + focusNode: myNameFocusNode, + showQrScanOption: false, + label: "My name", + hint: "Enter your name", + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 6, + ), + Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + child: Text( + "Enter your name EXACTLY as the group creator entered it. " + "The names are case-sensitive.", + style: STextStyles.label(context), + ), + ), + ), + ], + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has joined the group", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start key generation", + enabled: _userVerifyContinue && !_nameEmpty && !_configEmpty, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final config = configFieldController.text; + + if (!Frost.validateEncodedMultisigConfig(encodedConfig: config)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Invalid config", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + if (!Frost.getParticipants(multisigConfig: config) + .contains(myNameFieldController.text)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "My name not found in config participants", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + ref.read(pFrostMyName.state).state = myNameFieldController.text; + ref.read(pFrostMultisigConfig.notifier).state = config; + + ref.read(pFrostStartKeyGenData.state).state = + Frost.startKeyGeneration( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + myName: ref.read(pFrostMyName.state).state!, + ); + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart new file mode 100644 index 000000000..f993d6783 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_2.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostCreateStep2 extends ConsumerStatefulWidget { + const FrostCreateStep2({ + super.key, + }); + + static const String routeName = "/frostCreateStep2"; + static const String title = "Commitments"; + + @override + ConsumerState createState() => _FrostCreateStep2State(); +} + +class _FrostCreateStep2State extends ConsumerState { + static const info = [ + "Share your commitment with other group members.", + "Enter their commitments into the corresponding fields.", + ]; + + final List controllers = []; + final List focusNodes = []; + + late final List participants; + late final String myCommitment; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + bool _userVerifyContinue = false; + + @override + void initState() { + participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); + myCommitment = ref.read(pFrostStartKeyGenData.state).state!.commitments; + + // temporarily remove my name + participants.removeAt(myIndex); + + for (int i = 0; i < participants.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 12), + DetailItem( + title: "My name", + detail: ref.watch(pFrostMyName.state).state!, + ), + const SizedBox(height: 12), + DetailItem( + title: "My commitment", + detail: myCommitment, + button: Util.isDesktop + ? IconCopyButton( + data: myCommitment, + ) + : SimpleCopyButton( + data: myCommitment, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myCommitment, + ), + const SizedBox(height: 12), + for (int i = 0; i < participants.length; i++) + Padding( + padding: const EdgeInsets.only(top: 12), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: participants[i], + hint: "Enter ${participants[i]}'s commitment", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + CheckboxTextButton( + label: "I have verified that everyone has my commitment", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox(height: 12), + PrimaryButton( + label: "Generate shares", + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: () async { + // check for empty commitments + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing commitments", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect commitment strings and insert my own at the correct index + final commitments = controllers.map((e) => e.text).toList(); + commitments.insert(myIndex, myCommitment); + + try { + ref.read(pFrostSecretSharesData.notifier).state = + Frost.generateSecretShares( + multisigConfigWithNamePtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .multisigConfigWithNamePtr, + mySeed: ref.read(pFrostStartKeyGenData.state).state!.seed, + secretShareMachineWrapperPtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .secretShareMachineWrapperPtr, + commitments: commitments, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: "Failed to generate shares", + message: e.toString(), + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart new file mode 100644 index 000000000..54cabcec0 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_3.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostCreateStep3 extends ConsumerStatefulWidget { + const FrostCreateStep3({super.key}); + + static const String routeName = "/frostCreateStep3"; + static const String title = "Shares"; + + @override + ConsumerState createState() => _FrostCreateStep3State(); +} + +class _FrostCreateStep3State extends ConsumerState { + static const info = [ + "Send your share to other group members.", + "Enter their shares into the corresponding fields.", + ]; + + bool _userVerifyContinue = false; + + final List controllers = []; + final List focusNodes = []; + + late final List participants; + late final String myShare; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + + @override + void initState() { + participants = Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ); + myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!); + myShare = ref.read(pFrostSecretSharesData.state).state!.share; + + // temporarily remove my name. Added back later + participants.removeAt(myIndex); + + for (int i = 0; i < participants.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 12), + DetailItem( + title: "My name", + detail: ref.watch(pFrostMyName.state).state!, + ), + const SizedBox(height: 12), + DetailItem( + title: "My share", + detail: myShare, + button: Util.isDesktop + ? IconCopyButton( + data: myShare, + ) + : SimpleCopyButton( + data: myShare, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myShare, + ), + const SizedBox(height: 12), + for (int i = 0; i < participants.length; i++) + Padding( + padding: const EdgeInsets.only(top: 12), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: participants[i], + hint: "Enter ${participants[i]}'s share", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + CheckboxTextButton( + label: "I have verified that everyone has my share", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Generate", + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: () async { + // check for empty commitments + if (controllers + .map((e) => e.text.isEmpty) + .reduce((value, element) => value |= element)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Missing shares", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + // collect commitment strings and insert my own at the correct index + final shares = controllers.map((e) => e.text).toList(); + shares.insert(myIndex, myShare); + + try { + ref.read(pFrostCompletedKeyGenData.notifier).state = + Frost.completeKeyGeneration( + multisigConfigWithNamePtr: ref + .read(pFrostStartKeyGenData.state) + .state! + .multisigConfigWithNamePtr, + secretSharesResPtr: ref + .read(pFrostSecretSharesData.state) + .state! + .secretSharesResPtr, + shares: shares, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 4; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => const FrostErrorDialog( + title: "Failed to complete key generation", + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart new file mode 100644 index 000000000..864e905bf --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_4.dart @@ -0,0 +1,77 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; + +class FrostCreateStep4 extends ConsumerStatefulWidget { + const FrostCreateStep4({super.key}); + + static const String routeName = "/frostCreateStep4"; + static const String title = "Verify multisig ID"; + + @override + ConsumerState createState() => _FrostCreateStep4State(); +} + +class _FrostCreateStep4State extends ConsumerState { + static const info = [ + "Ensure your multisig ID matches that of each other participant.", + ]; + + late final Uint8List multisigId; + + @override + void initState() { + multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 12), + DetailItem( + title: "Multisig ID", + detail: multisigId.toString(), + button: Util.isDesktop + ? IconCopyButton( + data: multisigId.toString(), + ) + : SimpleCopyButton( + data: multisigId.toString(), + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + PrimaryButton( + label: "Confirm", + onPressed: () { + ref.read(pFrostCreateCurrentStep.state).state = 5; + Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart new file mode 100644 index 000000000..586a1c189 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -0,0 +1,239 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/node_service_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class FrostCreateStep5 extends ConsumerStatefulWidget { + const FrostCreateStep5({super.key}); + + static const String routeName = "/frostCreateStep5"; + static const String title = "Back up your keys"; + + @override + ConsumerState createState() => _FrostCreateStep5State(); +} + +class _FrostCreateStep5State extends ConsumerState { + static const _warning = "These are your private keys. Please back them up, " + "keep them safe and never share it with anyone. Your private keys are the" + " only way you can access your funds if you forget PIN, lose your phone, " + "etc. Stack Wallet does not keep nor is able to restore your private keys" + "."; + + late final String seed, recoveryString, serializedKeys, multisigConfig; + late final Uint8List multisigId; + + bool _userVerifyContinue = false; + + @override + void initState() { + seed = ref.read(pFrostStartKeyGenData.state).state!.seed; + serializedKeys = + ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys; + recoveryString = + ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString; + multisigConfig = ref.read(pFrostMultisigConfig.state).state!; + multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + RoundedContainer( + color: + Theme.of(context).extension()!.warningBackground, + child: Text( + _warning, + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .warningForeground, + ), + ), + ), + const SizedBox(height: 12), + DetailItem( + title: "Multisig Config", + detail: multisigConfig, + button: Util.isDesktop + ? IconCopyButton( + data: multisigConfig, + ) + : SimpleCopyButton( + data: multisigConfig, + ), + ), + const SizedBox(height: 12), + DetailItem( + title: "Keys", + detail: serializedKeys, + button: Util.isDesktop + ? IconCopyButton( + data: serializedKeys, + ) + : SimpleCopyButton( + data: serializedKeys, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox(height: 12), + CheckboxTextButton( + label: "I have backed up my keys and the config", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox(height: 12), + PrimaryButton( + label: "Continue", + enabled: _userVerifyContinue, + onPressed: () async { + bool progressPopped = false; + try { + unawaited( + showDialog( + context: context, + barrierDismissible: false, + useSafeArea: true, + builder: (ctx) { + return const Center( + child: LoadingIndicator( + width: 50, + height: 50, + ), + ); + }, + ), + ); + + final data = ref.read(pFrostScaffoldArgs)!; + + final info = WalletInfo.createNew( + coin: data.info.frostCurrency.coin, + name: data.info.walletName, + ); + + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonic: seed, + mnemonicPassphrase: "", + ); + + await (wallet as BitcoinFrostWallet).initializeNewFrost( + multisigConfig: multisigConfig, + recoveryString: recoveryString, + serializedKeys: serializedKeys, + multisigId: multisigId, + myName: ref.read(pFrostMyName.state).state!, + participants: Frost.getParticipants( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ), + threshold: Frost.getThreshold( + multisigConfig: ref.read(pFrostMultisigConfig.state).state!, + ), + ); + + await info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + ref.read(pWallets).addWallet(wallet); + + // pop progress dialog + if (context.mounted) { + Navigator.pop(context); + progressPopped = true; + } + + if (mounted) { + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + final nav = ref.read(pFrostScaffoldArgs)!.parentNav; + + if (Util.isDesktop) { + nav.popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + unawaited( + nav.pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + } + + ref.read(pFrostMultisigConfig.state).state = null; + ref.read(pFrostStartKeyGenData.state).state = null; + ref.read(pFrostSecretSharesData.state).state = null; + ref.read(pFrostScaffoldArgs.state).state = null; + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Your wallet is set up.", + iconAsset: Assets.svg.check, + context: nav.context, + ), + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + // pop progress dialog + if (context.mounted && !progressPopped) { + Navigator.pop(context); + progressPopped = true; + } + // TODO: handle gracefully + rethrow; + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart new file mode 100644 index 000000000..3b20a8820 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1a.dart @@ -0,0 +1,308 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; + +class FrostReshareStep1a extends ConsumerStatefulWidget { + const FrostReshareStep1a({super.key}); + + static const String routeName = "/frostReshareStep1a"; + static const String title = "Resharer config"; + + @override + ConsumerState createState() => _FrostReshareStep1aState(); +} + +class _FrostReshareStep1aState extends ConsumerState { + static const info = [ + "Share this config with the signing group participants as well as any new " + "participant.", + "Wait for them to import the config.", + "Verify that everyone has imported the config. If you try to continue " + "before everyone is ready, the process will be canceled.", + "Check the box and press “Start resharing”.", + ]; + + late final bool iAmInvolved; + + bool _buttonLock = false; + bool _userVerifyContinue = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; + + final serializedKeys = await wallet.getSerializedKeys(); + if (mounted) { + final result = Frost.beginResharer( + serializedKeys: serializedKeys!, + config: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + ); + + ref.read(pFrostResharingData).startResharerData = result; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: e.toString(), + ), + ); + } + } finally { + _buttonLock = false; + } + } + + void _showParticipantsDialog() { + final participants = + ref.read(pFrostResharingData).configData!.newParticipants; + + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + showCloseButton: false, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "Group participants", + style: STextStyles.w600_20(context), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + "The names are case-sensitive and must be entered exactly.", + style: STextStyles.w400_16(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + ), + const SizedBox( + height: 12, + ), + for (final participant in participants) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + height: 1.5, + color: + Theme.of(context).extension()!.background, + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + participant, + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: participant, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + const SizedBox( + height: 24, + ), + ], + ), + ), + ); + } + + @override + void initState() { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(ref.read(pFrostScaffoldArgs)!.walletId!)!; + + final myOldIndex = frostInfo.participants.indexOf(frostInfo.myName); + + iAmInvolved = ref + .read(pFrostResharingData) + .configData! + .resharers + .values + .contains(myOldIndex); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), + SizedBox( + height: 220, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostResharingData).resharerRConfig!, + size: 220, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + DetailItem( + title: "Config", + detail: ref.watch(pFrostResharingData).resharerRConfig!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostResharingData).resharerRConfig!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostResharingData).resharerRConfig!, + ), + ), + SizedBox( + height: Util.isDesktop ? 64 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show group participants", + onPressed: _showParticipantsDialog, + ), + ), + ], + ), + if (iAmInvolved && !Util.isDesktop) const Spacer(), + if (iAmInvolved) + const SizedBox( + height: 16, + ), + if (iAmInvolved) + CheckboxTextButton( + label: "I have verified that everyone has imported the config", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + if (iAmInvolved) + const SizedBox( + height: 16, + ), + if (iAmInvolved) + PrimaryButton( + label: "Start resharing", + enabled: _userVerifyContinue, + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart new file mode 100644 index 000000000..0df4c4b09 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1b.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frostdart/frostdart.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep1b extends ConsumerStatefulWidget { + const FrostReshareStep1b({ + super.key, + }); + + static const String routeName = "/frostReshareStep1b"; + static const String title = "Import reshare config"; + + @override + ConsumerState createState() => _FrostReshareStep1bState(); +} + +class _FrostReshareStep1bState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group member who" + " is initiating resharing.", + "Wait for other participants to finish importing the config.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be canceled.", + "Check the box and press “Start resharing”.", + ]; + + late final TextEditingController configFieldController; + late final FocusNode configFocusNode; + + bool _configEmpty = true; + + bool _buttonLock = false; + bool _userVerifyContinue = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + final walletId = ref.read(pFrostScaffoldArgs)!.walletId!; + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; + + ref.read(pFrostResharingData).reset(); + ref.read(pFrostResharingData).myName = frostInfo.myName; + ref.read(pFrostResharingData).resharerRConfig = + configFieldController.text; + + String? salt; + try { + salt = Format.uint8listToString( + resharerSalt( + resharerConfig: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + ), + ); + } catch (_) { + throw Exception("Bad resharer config"); + } + + if (frostInfo.knownSalts.contains(salt)) { + throw Exception("Duplicate config salt"); + } else { + final salts = frostInfo.knownSalts.toList(); + salts.add(salt); + final mainDB = ref.read(mainDBProvider); + await mainDB.isar.writeTxn(() async { + final id = frostInfo.id; + await mainDB.isar.frostWalletInfo.delete(id); + await mainDB.isar.frostWalletInfo.put( + frostInfo.copyWith(knownSalts: salts), + ); + }); + } + + final serializedKeys = await ref.read(secureStoreProvider).read( + key: "{$walletId}_serializedFROSTKeys", + ); + if (mounted) { + final result = Frost.beginResharer( + serializedKeys: serializedKeys!, + config: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + ); + + ref.read(pFrostResharingData).startResharerData = result; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: e.toString(), + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + configFieldController = TextEditingController(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + configFieldController.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 16, + ), + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Enter config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has imported the config", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Start resharing", + enabled: !_configEmpty && _userVerifyContinue, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + await _onPressed(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart new file mode 100644 index 000000000..2bbfef1de --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_1c.dart @@ -0,0 +1,229 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep1c extends ConsumerStatefulWidget { + const FrostReshareStep1c({super.key}); + + static const String routeName = "/frostReshareStep1c"; + static const String title = "Import reshare config"; + + @override + ConsumerState createState() => _FrostReshareStep1cState(); +} + +class _FrostReshareStep1cState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the group creator.", + "Enter your name EXACTLY as the group creator entered it. When in doubt, " + "double check with them. The names are case-sensitive.", + "Wait for other participants to finish entering their information.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process could be canceled.", + "Check the box and press “Join group”.", + ]; + + late final TextEditingController myNameFieldController, configFieldController; + late final FocusNode myNameFocusNode, configFocusNode; + + bool _nameEmpty = true, + _configEmpty = true, + _userVerifyContinue = false, + _buttonLock = false; + + Future _createWallet() async { + final data = ref.read(pFrostScaffoldArgs)!; + + final info = WalletInfo.createNew( + name: data.info.walletName, + coin: data.info.frostCurrency.coin, + ); + + final wallet = IncompleteFrostWallet(); + wallet.info = info; + + return wallet; + } + + @override + void initState() { + myNameFieldController = TextEditingController(); + configFieldController = TextEditingController(); + myNameFocusNode = FocusNode(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + myNameFieldController.dispose(); + configFieldController.dispose(); + myNameFocusNode.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 16, + ), + FrostStepField( + controller: myNameFieldController, + focusNode: myNameFocusNode, + showQrScanOption: false, + label: "My name", + hint: "Enter your name", + onChanged: (_) { + setState(() { + _nameEmpty = myNameFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 16, + ), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Enter config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has joined the group", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Join group", + enabled: _userVerifyContinue && !_nameEmpty && !_configEmpty, + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + ref.read(pFrostResharingData).reset(); + ref.read(pFrostResharingData).myName = + myNameFieldController.text; + ref.read(pFrostResharingData).resharerRConfig = + configFieldController.text; + + if (!ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!)) { + ref.read(pFrostResharingData).reset(); + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "My name not found in config participants", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + Exception? ex; + final wallet = await showLoading( + whileFuture: _createWallet(), + context: context, + message: "Setting up wallet", + rootNavigator: true, + onException: (e) => ex = e, + ); + + if (ex != null) { + throw ex!; + } + + if (context.mounted) { + ref.read(pFrostResharingData).incompleteWallet = wallet!; + final data = ref.read(pFrostScaffoldArgs)!; + ref.read(pFrostScaffoldArgs.state).state = ( + info: data.info, + walletId: wallet.walletId, + stepRoutes: data.stepRoutes, + parentNav: data.parentNav, + frostInterruptionDialogType: + FrostInterruptionDialogType.resharing, + callerRouteName: data.callerRouteName, + ); + ref.read(pFrostMyName.state).state = + ref.read(pFrostResharingData).myName!; + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (context.mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart new file mode 100644 index 000000000..1a688bb03 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2abd.dart @@ -0,0 +1,217 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep2abd extends ConsumerStatefulWidget { + const FrostReshareStep2abd({super.key}); + + static const String routeName = "/FrostReshareStep2abd"; + static const String title = "Resharers"; + + @override + ConsumerState createState() => + _FrostReshareStep2abdState(); +} + +class _FrostReshareStep2abdState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final Map resharers; + late final int myResharerIndexIndex; + late final String myResharerStart; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + + bool _userVerifyContinue = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + if (!amOutgoingParticipant) { + // collect resharer strings + final resharerStarts = controllers.map((e) => e.text).toList(); + if (myResharerIndexIndex >= 0) { + // only insert my own at the correct index if I am a resharer + resharerStarts.insert(myResharerIndexIndex, myResharerStart); + } + + final result = Frost.beginReshared( + myName: ref.read(pFrostResharingData).myName!, + resharerConfig: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + resharerStarts: resharerStarts, + ); + + ref.read(pFrostResharingData).startResharedData = result; + } + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: "Error", + message: e.toString(), + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(ref.read(pFrostScaffoldArgs)!.walletId!)!; + final myOldIndex = + frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); + + myResharerStart = + ref.read(pFrostResharingData).startResharerData!.resharerStart; + + resharers = ref.read(pFrostResharingData).configData!.resharers; + myResharerIndexIndex = resharers.values.toList().indexOf(myOldIndex); + if (myResharerIndexIndex >= 0) { + // remove my name for now as we don't need a text field for it + resharers.remove(ref.read(pFrostResharingData).myName!); + } + + amOutgoingParticipant = !ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!); + + for (int i = 0; i < resharers.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + DetailItem( + title: "My resharer", + detail: myResharerStart, + button: Util.isDesktop + ? IconCopyButton( + data: myResharerStart, + ) + : SimpleCopyButton( + data: myResharerStart, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myResharerStart, + ), + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharers.length; i++) + FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: resharers.keys.elementAt(i), + hint: "Enter " + "${resharers.keys.elementAt(i)}" + "'s resharer", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + CheckboxTextButton( + label: "I have verified that everyone has my resharer", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: _userVerifyContinue && + (amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e)), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart new file mode 100644 index 000000000..798c503e5 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_2c.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep2c extends ConsumerStatefulWidget { + const FrostReshareStep2c({super.key}); + + static const String routeName = "/FrostReshareStep2c"; + static const String title = "Resharers"; + + @override + ConsumerState createState() => _FrostReshareStep2cState(); +} + +class _FrostReshareStep2cState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final Map resharers; + + final List fieldIsEmptyFlags = []; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // collect resharer strings + final resharerStarts = controllers.map((e) => e.text).toList(); + + final result = Frost.beginReshared( + myName: ref.read(pFrostResharingData).myName!, + resharerConfig: Frost.decodeRConfig( + ref.read(pFrostResharingData).resharerRConfig!, + ), + resharerStarts: resharerStarts, + ); + + ref.read(pFrostResharingData).startResharedData = result; + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: "Error", + message: e.toString(), + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + resharers = ref.read(pFrostResharingData).configData!.resharers; + + for (int i = 0; i < resharers.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharers.length; i++) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: resharers.keys.elementAt(i), + hint: "Enter " + "${resharers.keys.elementAt(i)}" + "'s resharer", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart new file mode 100644 index 000000000..d49df3cc7 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3abd.dart @@ -0,0 +1,208 @@ +import 'dart:ffi'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostReshareStep3abd extends ConsumerStatefulWidget { + const FrostReshareStep3abd({super.key}); + + static const String routeName = "/frostReshareStep3abd"; + static const String title = "Encryption keys"; + + @override + ConsumerState createState() => + _FrostReshareStep3abdState(); +} + +class _FrostReshareStep3abdState extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final List newParticipants; + late final int myIndex; + late final String? myEncryptionKey; + late final bool amOutgoingParticipant; + + final List fieldIsEmptyFlags = []; + + bool _userVerifyContinue = false; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // collect encryptionKeys strings and insert my own at the correct index + final encryptionKeys = controllers.map((e) => e.text).toList(); + if (!amOutgoingParticipant) { + encryptionKeys.insert(myIndex, myEncryptionKey!); + } + + final result = Frost.finishResharer( + machine: ref.read(pFrostResharingData).startResharerData!.machine.ref, + encryptionKeysOfResharedTo: encryptionKeys, + ); + + ref.read(pFrostResharingData).resharerComplete = result; + + ref.read(pFrostCreateCurrentStep.state).state = 4; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: "Error", + message: e.toString(), + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + myEncryptionKey = + ref.read(pFrostResharingData).startResharedData?.resharedStart; + + newParticipants = ref.read(pFrostResharingData).configData!.newParticipants; + myIndex = newParticipants.indexOf(ref.read(pFrostResharingData).myName!); + + if (myIndex >= 0) { + // remove my name for now as we don't need a text field for it + newParticipants.removeAt(myIndex); + } + + if (myEncryptionKey == null && myIndex == -1) { + amOutgoingParticipant = true; + } else if (myEncryptionKey != null && myIndex >= 0) { + amOutgoingParticipant = false; + } else { + throw Exception("Invalid resharing state"); + } + + for (int i = 0; i < newParticipants.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + if (!amOutgoingParticipant) + DetailItem( + title: "My encryption key", + detail: myEncryptionKey!, + button: Util.isDesktop + ? IconCopyButton( + data: myEncryptionKey!, + ) + : SimpleCopyButton( + data: myEncryptionKey!, + ), + ), + if (!amOutgoingParticipant) const SizedBox(height: 12), + if (!amOutgoingParticipant) + FrostQrDialogPopupButton( + data: myEncryptionKey!, + ), + if (!amOutgoingParticipant) + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < newParticipants.length; i++) + Padding( + padding: const EdgeInsets.only(top: 12), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: newParticipants[i], + hint: "Enter " + "${newParticipants[i]}" + "'s encryption key", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + if (!amOutgoingParticipant) + const SizedBox( + height: 12, + ), + if (!amOutgoingParticipant) + CheckboxTextButton( + label: "I have verified that everyone has my encryption key", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: (amOutgoingParticipant || _userVerifyContinue) && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart new file mode 100644 index 000000000..3bde7bc76 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_3c.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; + +class FrostReshareStep3c extends ConsumerStatefulWidget { + const FrostReshareStep3c({super.key}); + + static const String routeName = "/frostReshareStep3c"; + static const String title = "Encryption keys"; + + @override + ConsumerState createState() => _FrostReshareStep3cState(); +} + +class _FrostReshareStep3cState extends ConsumerState { + bool _userVerifyContinue = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + DetailItem( + title: "My encryption key", + detail: + ref.watch(pFrostResharingData).startResharedData!.resharedStart, + button: Util.isDesktop + ? IconCopyButton( + data: ref + .watch(pFrostResharingData) + .startResharedData! + .resharedStart, + ) + : SimpleCopyButton( + data: ref + .watch(pFrostResharingData) + .startResharedData! + .resharedStart, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: + ref.watch(pFrostResharingData).startResharedData!.resharedStart, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + CheckboxTextButton( + label: "I have verified that everyone has my encryption key", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + enabled: _userVerifyContinue, + onPressed: () { + ref.read(pFrostCreateCurrentStep.state).state = 4; + Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart new file mode 100644 index 000000000..12de74573 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_4.dart @@ -0,0 +1,247 @@ +import 'dart:ffi'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +// was FinishResharingView +class FrostReshareStep4 extends ConsumerStatefulWidget { + const FrostReshareStep4({super.key}); + + static const String routeName = "/frostReshareStep4"; + static const String title = "Resharer completes"; + + @override + ConsumerState createState() => _FrostReshareStep4State(); +} + +class _FrostReshareStep4State extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final Map resharers; + late final String myName; + late final int? myResharerIndexIndex; + late final String? myResharerComplete; + late final bool amOutgoingParticipant; + late final bool amNewParticipant; + + final List fieldIsEmptyFlags = []; + + bool _userVerifyContinue = false; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + if (amOutgoingParticipant) { + ref.read(pFrostResharingData).reset(); + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + ref.read(pFrostScaffoldArgs)?.parentNav.popUntil( + ModalRoute.withName( + Util.isDesktop + ? DesktopWalletView.routeName + : WalletView.routeName, + ), + ); + } else { + // collect resharer completes strings and insert my own at the correct index + final resharerCompletes = controllers.map((e) => e.text).toList(); + if (myResharerIndexIndex != null && myResharerComplete != null) { + resharerCompletes.insert(myResharerIndexIndex!, myResharerComplete!); + } + + final data = Frost.finishReshared( + prior: ref.read(pFrostResharingData).startResharedData!.prior.ref, + resharerCompletes: resharerCompletes, + ); + + ref.read(pFrostResharingData).newWalletData = data; + + ref.read(pFrostCreateCurrentStep.state).state = 5; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: "Error", + message: e.toString(), + ), + ); + } + } finally { + _buttonLock = false; + } + } + + @override + void initState() { + amNewParticipant = + ref.read(pFrostResharingData).startResharerData == null && + ref.read(pFrostResharingData).incompleteWallet != null && + ref.read(pFrostResharingData).incompleteWallet?.walletId == + ref.read(pFrostScaffoldArgs)!.walletId!; + + myName = ref.read(pFrostResharingData).myName!; + + resharers = ref.read(pFrostResharingData).configData!.resharers; + + if (amNewParticipant) { + myResharerComplete = null; + myResharerIndexIndex = null; + amOutgoingParticipant = false; + } else { + myResharerComplete = ref.read(pFrostResharingData).resharerComplete!; + + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(ref.read(pFrostScaffoldArgs)!.walletId!)!; + final myOldIndex = + frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!); + + myResharerIndexIndex = resharers.values.toList().indexOf(myOldIndex); + if (myResharerIndexIndex! >= 0) { + // remove my name for now as we don't need a text field for it + resharers.remove(ref.read(pFrostResharingData).myName!); + } + + amOutgoingParticipant = !ref + .read(pFrostResharingData) + .configData! + .newParticipants + .contains(ref.read(pFrostResharingData).myName!); + } + + for (int i = 0; i < resharers.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + if (myResharerComplete != null) + DetailItem( + title: "My resharer complete", + detail: myResharerComplete!, + button: Util.isDesktop + ? IconCopyButton( + data: myResharerComplete!, + ) + : SimpleCopyButton( + data: myResharerComplete!, + ), + ), + if (myResharerComplete != null) const SizedBox(height: 12), + if (myResharerComplete != null) + FrostQrDialogPopupButton( + data: myResharerComplete!, + ), + if (!amOutgoingParticipant) + const SizedBox( + height: 16, + ), + if (!amOutgoingParticipant) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < resharers.length; i++) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: FrostStepField( + controller: controllers[i], + focusNode: focusNodes[i], + showQrScanOption: true, + label: resharers.keys.elementAt(i), + hint: "Enter " + "${resharers.keys.elementAt(i)}" + "'s resharer", + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + if (!amNewParticipant) + CheckboxTextButton( + label: "I have verified that everyone has my resharer complete", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + if (!amNewParticipant) + const SizedBox( + height: 16, + ), + PrimaryButton( + label: amOutgoingParticipant ? "Done" : "Complete", + enabled: (amNewParticipant || _userVerifyContinue) && + (amOutgoingParticipant || + !fieldIsEmptyFlags.reduce((v, e) => v |= e)), + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart new file mode 100644 index 000000000..fe0875747 --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/reshare/frost_reshare_step_5.dart @@ -0,0 +1,220 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/node_service_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; + +// was VerifyUpdatedWalletView +class FrostReshareStep5 extends ConsumerStatefulWidget { + const FrostReshareStep5({super.key}); + + static const String routeName = "/frostReshareStep5"; + static const String title = "Verify"; + + @override + ConsumerState createState() => _FrostReshareStep5State(); +} + +class _FrostReshareStep5State extends ConsumerState { + late final String config; + late final String serializedKeys; + late final String reshareId; + + late final bool isNew; + + bool _buttonLock = false; + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + Exception? ex; + + final BitcoinFrostWallet wallet; + + if (isNew) { + wallet = await ref + .read(pFrostResharingData) + .incompleteWallet! + .toBitcoinFrostWallet( + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + ); + + await wallet.info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + ref.read(pWallets).addWallet(wallet); + } else { + wallet = ref + .read(pWallets) + .getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; + } + + if (mounted) { + await showLoading( + whileFuture: wallet.updateWithResharedData( + serializedKeys: serializedKeys, + multisigConfig: config, + isNewWallet: isNew, + ), + context: context, + message: isNew ? "Creating wallet" : "Updating wallet data", + rootNavigator: true, + onException: (e) => ex = e, + ); + + if (ex != null) { + throw ex!; + } + + if (mounted) { + ref.read(pFrostResharingData).reset(); + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + ref.read(pFrostScaffoldArgs)?.parentNav.popUntil( + ModalRoute.withName( + _popUntilPath, + ), + ); + } + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => FrostErrorDialog( + title: "Error", + message: e.toString(), + ), + ); + } + } finally { + _buttonLock = false; + } + } + + String get _popUntilPath => isNew + ? Util.isDesktop + ? DesktopHomeView.routeName + : HomeView.routeName + : Util.isDesktop + ? DesktopWalletView.routeName + : WalletView.routeName; + + @override + void initState() { + config = ref.read(pFrostResharingData).newWalletData!.multisigConfig; + serializedKeys = + ref.read(pFrostResharingData).newWalletData!.serializedKeys; + reshareId = ref.read(pFrostResharingData).newWalletData!.resharedId; + + isNew = ref.read(pFrostResharingData).incompleteWallet != null && + ref.read(pFrostResharingData).incompleteWallet!.walletId == + ref.read(pFrostScaffoldArgs)!.walletId!; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Text( + "Ensure your reshare ID matches that of each other participant", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "ID", + detail: reshareId, + button: Util.isDesktop + ? IconCopyButton( + data: reshareId, + ) + : SimpleCopyButton( + data: reshareId, + ), + ), + const SizedBox( + height: 12, + ), + const SizedBox( + height: 12, + ), + Text( + "Back up your keys and config", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Config", + detail: config, + button: Util.isDesktop + ? IconCopyButton( + data: config, + ) + : SimpleCopyButton( + data: config, + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Keys", + detail: serializedKeys, + button: Util.isDesktop + ? IconCopyButton( + data: serializedKeys, + ) + : SimpleCopyButton( + data: serializedKeys, + ), + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Confirm", + onPressed: _onPressed, + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart new file mode 100644 index 000000000..68c220c1f --- /dev/null +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -0,0 +1,485 @@ +import 'dart:async'; + +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frostdart/frostdart.dart' as frost; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/global/node_service_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_mascot.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class RestoreFrostMsWalletView extends ConsumerStatefulWidget { + const RestoreFrostMsWalletView({ + super.key, + required this.walletName, + required this.frostCurrency, + }); + + static const String routeName = "/restoreFrostMsWalletView"; + + final String walletName; + final FrostCurrency frostCurrency; + + @override + ConsumerState createState() => + _RestoreFrostMsWalletViewState(); +} + +class _RestoreFrostMsWalletViewState + extends ConsumerState { + late final TextEditingController keysFieldController, configFieldController; + late final FocusNode keysFocusNode, configFocusNode; + + bool _keysEmpty = true, _configEmpty = true; + + bool _restoreButtonLock = false; + + Future _createWalletAndRecover() async { + final keys = keysFieldController.text; + final config = configFieldController.text; + + final myNameIndex = frost.getParticipantIndexFromKeys(serializedKeys: keys); + final participants = Frost.getParticipants(multisigConfig: config); + final myName = participants[myNameIndex]; + + final info = WalletInfo.createNew( + coin: widget.frostCurrency.coin, + name: widget.walletName, + ); + + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + ); + + final frostInfo = FrostWalletInfo( + walletId: info.walletId, + knownSalts: [], + participants: participants, + myName: myName, + threshold: frost.multisigThreshold( + multisigConfig: config, + ), + ); + + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.frostWalletInfo.put(frostInfo); + }); + + await (wallet as BitcoinFrostWallet).recover( + serializedKeys: keys, + multisigConfig: config, + isRescan: false, + ); + + await info.setMnemonicVerified( + isar: ref.read(mainDBProvider).isar, + ); + + return wallet; + } + + Future _restore() async { + if (_restoreButtonLock) { + return; + } + _restoreButtonLock = true; + + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + Exception? ex; + final wallet = await showLoading( + whileFuture: _createWalletAndRecover(), + context: context, + message: "Restoring wallet...", + rootNavigator: Util.isDesktop, + onException: (e) { + ex = e; + }, + ); + + if (ex != null) { + throw ex!; + } + + ref.read(pWallets).addWallet(wallet!); + + if (mounted) { + if (Util.isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + } + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Your wallet is set up.", + iconAsset: Assets.svg.check, + context: context, + ), + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Failed to restore", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _restoreButtonLock = false; + } + } + + @override + void initState() { + keysFieldController = TextEditingController(); + configFieldController = TextEditingController(); + keysFocusNode = FocusNode(); + configFocusNode = FocusNode(); + super.initState(); + } + + @override + void dispose() { + keysFieldController.dispose(); + configFieldController.dispose(); + keysFocusNode.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + // TODO: [prio=high] get rid of placeholder text?? + trailing: FrostMascot( + title: 'Lorem ipsum', + body: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ', + ), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Restore FROST multisig wallet", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frMyNameTextFieldKey"), + controller: keysFieldController, + onChanged: (_) { + setState(() { + _keysEmpty = keysFieldController.text.isEmpty; + }); + }, + focusNode: keysFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Keys", + keysFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _keysEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_keysEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Keys Field.", + key: const Key("frMyNameClearButtonKey"), + onTap: () { + keysFieldController.text = ""; + + setState(() { + _keysEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Keys Field.", + key: const Key("frKeysPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + keysFieldController.text = + data.text!.trim(); + } + + setState(() { + _keysEmpty = + keysFieldController.text.isEmpty; + }); + }, + child: _keysEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("frConfigTextFieldKey"), + controller: configFieldController, + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + focusNode: configFocusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter config", + configFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _configEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_configEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Config Field.", + key: const Key("frConfigClearButtonKey"), + onTap: () { + configFieldController.text = ""; + + setState(() { + _configEmpty = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Config Field Input.", + key: const Key("frConfigPasteButtonKey"), + onTap: () async { + final ClipboardData? data = + await Clipboard.getData( + Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + configFieldController.text = + data.text!.trim(); + } + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + }, + child: _configEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_configEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key("frConfigScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + configFieldController.text = + qrResult.rawContent; + + setState(() { + _configEmpty = + configFieldController.text.isEmpty; + }); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Restore", + enabled: !_keysEmpty && !_configEmpty, + onPressed: _restore, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart index 350c839f8..3cfeb6bbd 100644 --- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart +++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart @@ -14,6 +14,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart'; @@ -27,11 +30,15 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/name_generator.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/dice_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -77,6 +84,52 @@ class _NameYourWalletViewState extends ConsumerState { return name; } + Future _nextPressed() async { + final name = textEditingController.text; + + if (mounted) { + // hide keyboard if has focus + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 50)); + } + + if (mounted) { + ref.read(mnemonicWordCountStateProvider.state).state = + Constants.possibleLengthsForCoin(coin).last; + ref.read(pNewWalletOptions.notifier).state = null; + + switch (widget.addWalletType) { + case AddWalletType.New: + unawaited( + Navigator.of(context).pushNamed( + coin.hasMnemonicPassphraseSupport + ? NewWalletOptionsView.routeName + : NewWalletRecoveryPhraseWarningView.routeName, + arguments: Tuple2( + name, + coin, + ), + ), + ); + break; + + case AddWalletType.Restore: + unawaited( + Navigator.of(context).pushNamed( + RestoreOptionsView.routeName, + arguments: Tuple2( + name, + coin, + ), + ), + ); + break; + } + } + } + } + @override void initState() { isDesktop = Util.isDesktop; @@ -191,7 +244,7 @@ class _NameYourWalletViewState extends ConsumerState { height: isDesktop ? 0 : 16, ), Text( - "Name your ${coin.prettyName} wallet", + "Name your ${coin.prettyName} ${coin.isFrost ? "multisig " : ""}wallet", textAlign: TextAlign.center, style: isDesktop ? STextStyles.desktopH2(context) @@ -201,7 +254,7 @@ class _NameYourWalletViewState extends ConsumerState { height: isDesktop ? 16 : 8, ), Text( - "Enter a label for your wallet (e.g. Savings)", + "Enter a label for your wallet (e.g. ${coin.isFrost ? "Multisig" : "Savings"})", textAlign: TextAlign.center, style: isDesktop ? STextStyles.desktopSubtitleH2(context) @@ -330,78 +383,128 @@ class _NameYourWalletViewState extends ConsumerState { const SizedBox( height: 32, ), - ConstrainedBox( - constraints: BoxConstraints( - minWidth: isDesktop ? 480 : 0, - minHeight: isDesktop ? 70 : 0, + if (widget.coin.isFrost) + if (widget.addWalletType == AddWalletType.Restore) + PrimaryButton( + label: "Next", + enabled: _nextEnabled, + onPressed: () async { + final name = textEditingController.text; + + await Navigator.of(context).pushNamed( + RestoreFrostMsWalletView.routeName, + arguments: ( + walletName: name, + // TODO: [prio=med] this will cause issues if frost is ever applied to other coins + frostCurrency: coin.isTestNet + ? BitcoinFrost(CryptoCurrencyNetwork.test) + : BitcoinFrost(CryptoCurrencyNetwork.main), + ), + ); + }, + ), + if (widget.coin.isFrost && widget.addWalletType == AddWalletType.New) + Column( + children: [ + PrimaryButton( + label: "Create new group", + enabled: _nextEnabled, + onPressed: () async { + final name = textEditingController.text; + + await Navigator.of(context).pushNamed( + CreateNewFrostMsWalletView.routeName, + arguments: ( + walletName: name, + // TODO: [prio=med] this will cause issues if frost is ever applied to other coins + frostCurrency: coin.isTestNet + ? BitcoinFrost(CryptoCurrencyNetwork.test) + : BitcoinFrost(CryptoCurrencyNetwork.main), + ), + ); + }, + ), + const SizedBox( + height: 12, + ), + SecondaryButton( + label: "Join group", + enabled: _nextEnabled, + onPressed: () async { + final name = textEditingController.text; + + await Navigator.of(context).pushNamed( + SelectNewFrostImportTypeView.routeName, + arguments: ( + walletName: name, + // TODO: [prio=med] this will cause issues if frost is ever applied to other coins + frostCurrency: coin.isTestNet + ? BitcoinFrost(CryptoCurrencyNetwork.test) + : BitcoinFrost(CryptoCurrencyNetwork.main), + ), + ); + }, + ), + // SecondaryButton( + // label: "Import multisig config", + // enabled: _nextEnabled, + // onPressed: () async { + // final name = textEditingController.text; + // + // await Navigator.of(context).pushNamed( + // ImportNewFrostMsWalletView.routeName, + // arguments: ( + // walletName: name, + // coin: coin, + // ), + // ); + // }, + // ), + // const SizedBox( + // height: 12, + // ), + // SecondaryButton( + // label: "Import resharer config", + // enabled: _nextEnabled, + // onPressed: () async { + // final name = textEditingController.text; + // + // await Navigator.of(context).pushNamed( + // NewImportResharerConfigView.routeName, + // arguments: ( + // walletName: name, + // coin: coin, + // ), + // ); + // }, + // ), + ], ), - child: TextButton( - onPressed: _nextEnabled - ? () async { - final name = textEditingController.text; - - if (mounted) { - // hide keyboard if has focus - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 50)); - } - - if (mounted) { - ref.read(mnemonicWordCountStateProvider.state).state = - Constants.possibleLengthsForCoin(coin).last; - ref.read(pNewWalletOptions.notifier).state = null; - - switch (widget.addWalletType) { - case AddWalletType.New: - unawaited( - Navigator.of(context).pushNamed( - coin.hasMnemonicPassphraseSupport - ? NewWalletOptionsView.routeName - : NewWalletRecoveryPhraseWarningView - .routeName, - arguments: Tuple2( - name, - coin, - ), - ), - ); - break; - - case AddWalletType.Restore: - unawaited( - Navigator.of(context).pushNamed( - RestoreOptionsView.routeName, - arguments: Tuple2( - name, - coin, - ), - ), - ); - break; - } - } - } - } - : null, - style: _nextEnabled - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), - child: Text( - "Next", - style: isDesktop - ? _nextEnabled - ? STextStyles.desktopButtonEnabled(context) - : STextStyles.desktopButtonDisabled(context) - : STextStyles.button(context), + if (!widget.coin.isFrost) + ConstrainedBox( + constraints: BoxConstraints( + minWidth: isDesktop ? 480 : 0, + minHeight: isDesktop ? 70 : 0, + ), + child: TextButton( + onPressed: _nextEnabled ? _nextPressed : null, + style: _nextEnabled + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + child: Text( + "Next", + style: isDesktop + ? _nextEnabled + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.desktopButtonDisabled(context) + : STextStyles.button(context), + ), ), ), - ), if (isDesktop) const Spacer( flex: 15, diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index 44ac51aac..3c7bdfd9e 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -11,9 +11,7 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/restore_from_date_picker.dart'; @@ -24,7 +22,6 @@ import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widge import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/themes/theme_providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -33,6 +30,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/date_picker/date_picker.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/expandable.dart'; @@ -42,10 +40,10 @@ import 'package:tuple/tuple.dart'; class RestoreOptionsView extends ConsumerStatefulWidget { const RestoreOptionsView({ - Key? key, + super.key, required this.walletName, required this.coin, - }) : super(key: key); + }); static const routeName = "/restoreOptions"; @@ -68,7 +66,6 @@ class _RestoreOptionsViewState extends ConsumerState { final bool _nextEnabled = true; DateTime _restoreFromDate = DateTime.fromMillisecondsSinceEpoch(0); - late final Color baseColor; bool hidePassword = true; bool _expandedAdavnced = false; @@ -77,7 +74,6 @@ class _RestoreOptionsViewState extends ConsumerState { @override void initState() { - baseColor = ref.read(themeProvider.state).state.textSubtitle2; walletName = widget.walletName; coin = widget.coin; isDesktop = Util.isDesktop; @@ -99,52 +95,6 @@ class _RestoreOptionsViewState extends ConsumerState { super.dispose(); } - MaterialRoundedDatePickerStyle _buildDatePickerStyle() { - return MaterialRoundedDatePickerStyle( - paddingMonthHeader: const EdgeInsets.only(top: 11), - colorArrowNext: Theme.of(context).extension()!.textSubtitle1, - colorArrowPrevious: - Theme.of(context).extension()!.textSubtitle1, - textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), - textStyleDayHeader: STextStyles.datePicker600(context), - textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( - color: baseColor, - ), - textStyleDayOnCalendarDisabled: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle3, - ), - textStyleDayOnCalendarSelected: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.popupBG, - ), - textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textStyleYearButton: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - textStyleButtonAction: GoogleFonts.inter(), - ); - } - - MaterialRoundedYearPickerStyle _buildYearPickerStyle() { - return MaterialRoundedYearPickerStyle( - textStyleYear: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle2, - ), - textStyleYearSelected: STextStyles.datePicker600(context).copyWith( - fontSize: 18, - ), - ); - } - Future nextPressed() async { if (!isDesktop) { // hide keyboard if has focus @@ -169,67 +119,23 @@ class _RestoreOptionsViewState extends ConsumerState { } Future chooseDate() async { - final height = MediaQuery.of(context).size.height; - final fetchedColor = - Theme.of(context).extension()!.accentColorDark; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 125)); } - final date = await showRoundedDatePicker( - context: context, - initialDate: DateTime.now(), - height: height / 3.0, - theme: ThemeData( - primarySwatch: Util.createMaterialColor(fetchedColor), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _restoreFromDate = date; - _dateController.text = Format.formatDate(date); + if (mounted) { + final date = await showSWDatePicker(context); + if (date != null) { + _restoreFromDate = date; + _dateController.text = Format.formatDate(date); + } } } Future chooseDesktopDate() async { - final height = MediaQuery.of(context).size.height; - final fetchedColor = - Theme.of(context).extension()!.accentColorDark; - // check and hide keyboard - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed(const Duration(milliseconds: 125)); - } - - final date = await showRoundedDatePicker( - context: context, - initialDate: DateTime.now(), - height: height / 3.0, - theme: ThemeData( - primarySwatch: Util.createMaterialColor(fetchedColor), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); + final date = await showSWDatePicker(context); if (date != null) { _restoreFromDate = date; _dateController.text = Format.formatDate(date); diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index e9f0442d5..14174b22b 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -56,7 +56,6 @@ import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart'; import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; -import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; @@ -724,480 +723,459 @@ class _RestoreWalletViewState extends ConsumerState { ], ), body: Container( - color: Theme.of(context).extension()!.background, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: SingleChildScrollView( - controller: controller, - child: Column( - children: [ - /*if (isDesktop) + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: SingleChildScrollView( + controller: controller, + child: Column( + children: [ + /*if (isDesktop) const Spacer( flex: 10, ),*/ - if (!isDesktop) - Text( - widget.walletName, - style: STextStyles.itemSubtitle(context), - ), - SizedBox( - height: isDesktop ? 0 : 4, - ), + if (!isDesktop) Text( - "Recovery phrase", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), + widget.walletName, + style: STextStyles.itemSubtitle(context), ), - SizedBox( - height: isDesktop ? 16 : 8, - ), - Text( - "Enter your $_seedWordCount-word recovery phrase.", - style: isDesktop - ? STextStyles.desktopSubtitleH2(context) - : STextStyles.subtitle(context), - ), - SizedBox( - height: isDesktop ? 16 : 10, - ), - if (isDesktop) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: pasteMnemonic, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.clipboard, - width: 22, - height: 22, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - const SizedBox( - width: 8, - ), - Text( - "Paste", - style: STextStyles - .desktopButtonSmallSecondaryEnabled( - context), - ) - ], - ), + SizedBox( + height: isDesktop ? 0 : 4, + ), + Text( + "Recovery phrase", + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), + ), + SizedBox( + height: isDesktop ? 16 : 8, + ), + Text( + "Enter your $_seedWordCount-word recovery phrase.", + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), + ), + SizedBox( + height: isDesktop ? 16 : 10, + ), + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: pasteMnemonic, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12, ), - ), - ], - ), - if (isDesktop) - const SizedBox( - height: 20, - ), - if (isDesktop) - ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 1008, - ), - child: Builder( - builder: (BuildContext context) { - const cols = 4; - final int rows = _seedWordCount ~/ cols; - final int remainder = _seedWordCount % cols; - - return Column( + child: Row( children: [ - Form( - key: _formKey, - child: TableView( - shrinkWrap: true, - rowSpacing: 20, - rows: [ - for (int i = 0; i < rows; i++) - TableViewRow( - crossAxisAlignment: - CrossAxisAlignment.start, - spacing: 16, - cells: [ - for (int j = 1; j <= cols; j++) - TableViewCell( - flex: 1, - child: Column( - children: [ - TextFormField( - autocorrect: !isDesktop, - enableSuggestions: - !isDesktop, - textCapitalization: - TextCapitalization.none, - key: Key( - "restoreMnemonicFormField_$i"), - decoration: - _getInputDecorationFor( - _inputStatuses[ - i * 4 + j - 1], - "${i * 4 + j}"), - autovalidateMode: - AutovalidateMode - .onUserInteraction, - selectionControls: i * 4 + - j - - 1 == - 1 - ? textSelectionControls - : null, - // focusNode: - // _focusNodes[i * 4 + j - 1], - onChanged: (value) { - final FormInputStatus - formInputStatus; - - if (value.isEmpty) { - formInputStatus = - FormInputStatus - .empty; - } else if (_isValidMnemonicWord( - value - .trim() - .toLowerCase())) { - formInputStatus = - FormInputStatus - .valid; - } else { - formInputStatus = - FormInputStatus - .invalid; - } - - // if (formInputStatus == - // FormInputStatus.valid) { - // if (i * 4 + j < - // _focusNodes.length) { - // _focusNodes[i * 4 + j] - // .requestFocus(); - // } else if (i * 4 + j == - // _focusNodes.length) { - // _focusNodes[i * 4 + j - 1] - // .unfocus(); - // } - // } - setState(() { - _inputStatuses[ - i * 4 + j - 1] = - formInputStatus; - }); - }, - controller: _controllers[ - i * 4 + j - 1], - style: STextStyles.field( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textRestore, - fontSize: - isDesktop ? 16 : 14, - ), - ), - if (_inputStatuses[ - i * 4 + j - 1] == - FormInputStatus.invalid) - Align( - alignment: - Alignment.topLeft, - child: Padding( - padding: - const EdgeInsets - .only( - left: 12.0, - bottom: 4.0, - ), - child: Text( - "Please check spelling", - textAlign: - TextAlign.left, - style: - STextStyles.label( - context) - .copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textError, - ), - ), - ), - ) - ], - ), - ), - ], - expandingChild: null, - ), - if (remainder > 0) - TableViewRow( - spacing: 16, - cells: [ - for (int i = rows * cols; - i < _seedWordCount; - i++) ...[ - TableViewCell( - flex: 1, - child: Column( - children: [ - TextFormField( - autocorrect: !isDesktop, - enableSuggestions: - !isDesktop, - textCapitalization: - TextCapitalization.none, - key: Key( - "restoreMnemonicFormField_$i"), - decoration: - _getInputDecorationFor( - _inputStatuses[i], - "${i + 1}"), - autovalidateMode: - AutovalidateMode - .onUserInteraction, - selectionControls: i == 1 - ? textSelectionControls - : null, - // focusNode: _focusNodes[i], - onChanged: (value) { - final FormInputStatus - formInputStatus; - - if (value.isEmpty) { - formInputStatus = - FormInputStatus - .empty; - } else if (_isValidMnemonicWord( - value - .trim() - .toLowerCase())) { - formInputStatus = - FormInputStatus - .valid; - } else { - formInputStatus = - FormInputStatus - .invalid; - } - - // if (formInputStatus == - // FormInputStatus - // .valid && - // (i - 1) < - // _focusNodes.length) { - // Focus.of(context) - // .requestFocus( - // _focusNodes[i]); - // } - - // if (formInputStatus == - // FormInputStatus.valid) { - // if (i + 1 < - // _focusNodes.length) { - // _focusNodes[i + 1] - // .requestFocus(); - // } else if (i + 1 == - // _focusNodes.length) { - // _focusNodes[i].unfocus(); - // } - // } - }, - controller: _controllers[i], - style: STextStyles.field( - context) - .copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .overlay, - fontSize: - isDesktop ? 16 : 14, - ), - ), - if (_inputStatuses[i] == - FormInputStatus.invalid) - Align( - alignment: - Alignment.topLeft, - child: Padding( - padding: - const EdgeInsets - .only( - left: 12.0, - bottom: 4.0, - ), - child: Text( - "Please check spelling", - textAlign: - TextAlign.left, - style: - STextStyles.label( - context) - .copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textError, - ), - ), - ), - ) - ], - ), - ), - ], - for (int i = remainder; - i < cols; - i++) ...[ - TableViewCell( - flex: 1, - child: Container(), - ), - ], - ], - expandingChild: null, - ), - ], - ), + SvgPicture.asset( + Assets.svg.clipboard, + width: 22, + height: 22, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, ), const SizedBox( - height: 32, - ), - PrimaryButton( - label: "Restore wallet", - width: 480, - onPressed: requestRestore, + width: 8, ), + Text( + "Paste", + style: STextStyles + .desktopButtonSmallSecondaryEnabled( + context), + ) ], - ); - }, + ), + ), ), + ], + ), + if (isDesktop) + const SizedBox( + height: 20, + ), + if (isDesktop) + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 1008, ), - /*if (isDesktop) + child: Builder( + builder: (BuildContext context) { + const cols = 4; + final int rows = _seedWordCount ~/ cols; + final int remainder = _seedWordCount % cols; + + return Column( + children: [ + Form( + key: _formKey, + child: TableView( + shrinkWrap: true, + rowSpacing: 20, + rows: [ + for (int i = 0; i < rows; i++) + TableViewRow( + crossAxisAlignment: + CrossAxisAlignment.start, + spacing: 16, + cells: [ + for (int j = 1; j <= cols; j++) + TableViewCell( + flex: 1, + child: Column( + children: [ + TextFormField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + textCapitalization: + TextCapitalization.none, + key: Key( + "restoreMnemonicFormField_$i"), + decoration: + _getInputDecorationFor( + _inputStatuses[ + i * 4 + j - 1], + "${i * 4 + j}"), + autovalidateMode: + AutovalidateMode + .onUserInteraction, + selectionControls: i * 4 + + j - + 1 == + 1 + ? textSelectionControls + : null, + // focusNode: + // _focusNodes[i * 4 + j - 1], + onChanged: (value) { + final FormInputStatus + formInputStatus; + + if (value.isEmpty) { + formInputStatus = + FormInputStatus.empty; + } else if (_isValidMnemonicWord( + value + .trim() + .toLowerCase())) { + formInputStatus = + FormInputStatus.valid; + } else { + formInputStatus = + FormInputStatus + .invalid; + } + + // if (formInputStatus == + // FormInputStatus.valid) { + // if (i * 4 + j < + // _focusNodes.length) { + // _focusNodes[i * 4 + j] + // .requestFocus(); + // } else if (i * 4 + j == + // _focusNodes.length) { + // _focusNodes[i * 4 + j - 1] + // .unfocus(); + // } + // } + setState(() { + _inputStatuses[i * 4 + + j - + 1] = formInputStatus; + }); + }, + controller: _controllers[ + i * 4 + j - 1], + style: + STextStyles.field(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textRestore, + fontSize: + isDesktop ? 16 : 14, + ), + ), + if (_inputStatuses[ + i * 4 + j - 1] == + FormInputStatus.invalid) + Align( + alignment: + Alignment.topLeft, + child: Padding( + padding: + const EdgeInsets.only( + left: 12.0, + bottom: 4.0, + ), + child: Text( + "Please check spelling", + textAlign: + TextAlign.left, + style: + STextStyles.label( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textError, + ), + ), + ), + ) + ], + ), + ), + ], + expandingChild: null, + ), + if (remainder > 0) + TableViewRow( + spacing: 16, + cells: [ + for (int i = rows * cols; + i < _seedWordCount - remainder; + i++) ...[ + TableViewCell( + flex: 1, + child: Column( + // ... (existing code for input field) + ), + ), + ], + for (int i = _seedWordCount - remainder; + i < _seedWordCount; + i++) ...[ + TableViewCell( + flex: 1, + child: Column( + children: [ + TextFormField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + textCapitalization: + TextCapitalization.none, + key: Key( + "restoreMnemonicFormField_$i"), + decoration: + _getInputDecorationFor( + _inputStatuses[i], + "${i + 1}"), + autovalidateMode: + AutovalidateMode + .onUserInteraction, + selectionControls: i == 1 + ? textSelectionControls + : null, + onChanged: (value) { + final FormInputStatus + formInputStatus; + + if (value.isEmpty) { + formInputStatus = + FormInputStatus.empty; + } else if (_isValidMnemonicWord( + value + .trim() + .toLowerCase())) { + formInputStatus = + FormInputStatus.valid; + } else { + formInputStatus = + FormInputStatus + .invalid; + } + + setState(() { + _inputStatuses[i] = + formInputStatus; + }); + }, + controller: _controllers[i], + style: + STextStyles.field(context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .overlay, + fontSize: + isDesktop ? 16 : 14, + ), + ), + if (_inputStatuses[i] == + FormInputStatus.invalid) + Align( + alignment: + Alignment.topLeft, + child: Padding( + padding: + const EdgeInsets.only( + left: 12.0, + bottom: 4.0, + ), + child: Text( + "Please check spelling", + textAlign: + TextAlign.left, + style: + STextStyles.label( + context) + .copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textError, + ), + ), + ), + ) + ], + ), + ), + ], + for (int i = 0; + i < cols - remainder; + i++) ...[ + TableViewCell( + flex: 1, + child: Container(), + ), + ], + ], + expandingChild: null, + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + PrimaryButton( + label: "Restore wallet", + width: 480, + onPressed: requestRestore, + ), + ], + ); + }, + ), + ), + /*if (isDesktop) const Spacer( flex: 15, ),*/ - if (!isDesktop) - Padding( - padding: const EdgeInsets.all(4.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - for (int i = 1; i <= _seedWordCount; i++) - Column( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 4), - child: TextFormField( - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - textCapitalization: - TextCapitalization.none, - key: Key("restoreMnemonicFormField_$i"), - decoration: _getInputDecorationFor( - _inputStatuses[i - 1], "$i"), - autovalidateMode: - AutovalidateMode.onUserInteraction, - selectionControls: - i == 1 ? textSelectionControls : null, - // focusNode: _focusNodes[i - 1], - onChanged: (value) { - final FormInputStatus formInputStatus; + if (!isDesktop) + Padding( + padding: const EdgeInsets.all(4.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (int i = 1; i <= _seedWordCount; i++) + Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 4), + child: TextFormField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + textCapitalization: TextCapitalization.none, + key: Key("restoreMnemonicFormField_$i"), + decoration: _getInputDecorationFor( + _inputStatuses[i - 1], "$i"), + autovalidateMode: + AutovalidateMode.onUserInteraction, + selectionControls: + i == 1 ? textSelectionControls : null, + // focusNode: _focusNodes[i - 1], + onChanged: (value) { + final FormInputStatus formInputStatus; - if (value.isEmpty) { - formInputStatus = - FormInputStatus.empty; - } else if (_isValidMnemonicWord( - value.trim().toLowerCase())) { - formInputStatus = - FormInputStatus.valid; - } else { - formInputStatus = - FormInputStatus.invalid; - } + if (value.isEmpty) { + formInputStatus = FormInputStatus.empty; + } else if (_isValidMnemonicWord( + value.trim().toLowerCase())) { + formInputStatus = FormInputStatus.valid; + } else { + formInputStatus = + FormInputStatus.invalid; + } - // if (formInputStatus == - // FormInputStatus.valid) { - // if (i < _focusNodes.length) { - // _focusNodes[i].requestFocus(); - // } else if (i == _focusNodes.length) { - // _focusNodes[i - 1].unfocus(); - // } - // } - setState(() { - _inputStatuses[i - 1] = - formInputStatus; - }); - }, - controller: _controllers[i - 1], - style: - STextStyles.field(context).copyWith( - color: Theme.of(context) - .extension()! - .textRestore, - fontSize: isDesktop ? 16 : 14, - ), + // if (formInputStatus == + // FormInputStatus.valid) { + // if (i < _focusNodes.length) { + // _focusNodes[i].requestFocus(); + // } else if (i == _focusNodes.length) { + // _focusNodes[i - 1].unfocus(); + // } + // } + setState(() { + _inputStatuses[i - 1] = formInputStatus; + }); + }, + controller: _controllers[i - 1], + style: STextStyles.field(context).copyWith( + color: Theme.of(context) + .extension()! + .textRestore, + fontSize: isDesktop ? 16 : 14, ), ), - if (_inputStatuses[i - 1] == - FormInputStatus.invalid) - Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - bottom: 4.0, - ), - child: Text( - "Please check spelling", - textAlign: TextAlign.left, - style: STextStyles.label(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textError, - ), + ), + if (_inputStatuses[i - 1] == + FormInputStatus.invalid) + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + bottom: 4.0, + ), + child: Text( + "Please check spelling", + textAlign: TextAlign.left, + style: + STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension()! + .textError, ), ), - ) - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 8.0, - ), - child: PrimaryButton( - onPressed: requestRestore, - label: "Restore", - ), + ), + ) + ], ), - ], - ), + Padding( + padding: const EdgeInsets.only( + top: 8.0, + ), + child: PrimaryButton( + onPressed: requestRestore, + label: "Restore", + ), + ), + ], ), ), - ], - ), + ), + ], ), ), ), - ); + ), + ); } } diff --git a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart index 3963fc139..0b816cbe9 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart @@ -51,7 +51,7 @@ class RestoreSucceededDialog extends StatelessWidget { height: 16, ), Text( - "You can use your wallet now.", + "You may access your wallet now.", style: STextStyles.desktopTextMedium(context).copyWith( color: Theme.of(context).extension()!.textDark3, ), @@ -80,7 +80,7 @@ class RestoreSucceededDialog extends StatelessWidget { } else { return StackDialog( title: "Wallet restored", - message: "You can use your wallet now.", + message: "You may access your wallet now.", icon: SvgPicture.asset( Assets.svg.checkCircle, width: 24, diff --git a/lib/pages/address_book_views/address_book_view.dart b/lib/pages/address_book_views/address_book_view.dart index f302c287c..40e71434e 100644 --- a/lib/pages/address_book_views/address_book_view.dart +++ b/lib/pages/address_book_views/address_book_view.dart @@ -11,9 +11,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_book_filter_provider.dart'; @@ -23,6 +25,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/address_book_card.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -34,10 +37,10 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class AddressBookView extends ConsumerStatefulWidget { const AddressBookView({ - Key? key, + super.key, this.coin, this.filterTerm, - }) : super(key: key); + }); static const String routeName = "/addressBook"; @@ -61,10 +64,11 @@ class _AddressBookViewState extends ConsumerState { ref.refresh(addressBookFilterProvider); if (widget.coin == null) { - List coins = Coin.values.toList(); + final List coins = Coin.values.toList(); coins.remove(Coin.firoTestNet); - bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; + final bool showTestNet = + ref.read(prefsChangeNotifierProvider).showTestNetCoins; if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); @@ -78,13 +82,26 @@ class _AddressBookViewState extends ConsumerState { } WidgetsBinding.instance.addPostFrameCallback((_) async { - List addresses = []; + final List addresses = []; final wallets = ref.read(pWallets).wallets; for (final wallet in wallets) { + final String addressString; + if (wallet is SparkInterface) { + Address? address = await wallet.getCurrentReceivingSparkAddress(); + if (address == null) { + address = await wallet.generateNextSparkAddress(); + await ref.read(mainDBProvider).updateOrPutAddresses([address]); + } + addressString = address.value; + } else { + final address = await wallet.getCurrentReceivingAddress(); + addressString = address?.value ?? wallet.info.cachedReceivingAddress; + } + addresses.add( ContactAddressEntry() ..coinName = wallet.info.coin.name - ..address = (await wallet.getCurrentReceivingAddress())!.value + ..address = addressString ..label = "Current Receiving" ..other = wallet.info.name, ); @@ -302,15 +319,24 @@ class _AddressBookViewState extends ConsumerState { child: Column( children: [ ...contacts - .where((element) => element.addressesSorted - .where((e) => ref.watch(addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref - .read(addressBookServiceProvider) - .matches(widget.filterTerm ?? _searchTerm, e)) + .where( + (element) => element.addressesSorted + .where( + (e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin), + ), + ), + ) + .isNotEmpty, + ) + .where( + (e) => + e.isFavorite && + ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e), + ) .where((element) => element.isFavorite) .map( (e) => AddressBookCard( @@ -350,14 +376,22 @@ class _AddressBookViewState extends ConsumerState { child: Column( children: [ ...contacts - .where((element) => element.addressesSorted - .where((e) => ref.watch( - addressBookFilterProvider.select((value) => - value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => ref - .read(addressBookServiceProvider) - .matches(widget.filterTerm ?? _searchTerm, e)) + .where( + (element) => element.addressesSorted + .where( + (e) => ref.watch( + addressBookFilterProvider.select( + (value) => value.coins.contains(e.coin), + ), + ), + ) + .isNotEmpty, + ) + .where( + (e) => ref + .read(addressBookServiceProvider) + .matches(widget.filterTerm ?? _searchTerm, e), + ) .map( (e) => AddressBookCard( key: diff --git a/lib/pages/address_book_views/subviews/contact_popup.dart b/lib/pages/address_book_views/subviews/contact_popup.dart index ca91001c4..ae31f8c09 100644 --- a/lib/pages/address_book_views/subviews/contact_popup.dart +++ b/lib/pages/address_book_views/subviews/contact_popup.dart @@ -29,6 +29,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -39,10 +40,10 @@ final exchangeFromAddressBookAddressStateProvider = class ContactPopUp extends ConsumerWidget { const ContactPopUp({ - Key? key, + super.key, required this.contactId, this.clipboard = const ClipboardWrapper(), - }) : super(key: key); + }); final String contactId; final ClipboardInterface clipboard; @@ -384,13 +385,18 @@ class ContactPopUp extends ConsumerWidget { color: Theme.of(context) .extension()! .textFieldDefaultBG, - padding: - const EdgeInsets.all(4), + padding: EdgeInsets.all( + Util.isDesktop ? 4 : 6, + ), child: SvgPicture.asset( Assets .svg.circleArrowUpRight, - width: 12, - height: 12, + width: Util.isDesktop + ? 12 + : 16, + height: Util.isDesktop + ? 12 + : 16, color: Theme.of(context) .extension< StackColors>()! diff --git a/lib/pages/cashfusion/fusion_progress_view.dart b/lib/pages/cashfusion/fusion_progress_view.dart index 746825d36..78bc9ccc7 100644 --- a/lib/pages/cashfusion/fusion_progress_view.dart +++ b/lib/pages/cashfusion/fusion_progress_view.dart @@ -79,7 +79,7 @@ class _FusionProgressViewState extends ConsumerState { Future.delayed(const Duration(seconds: 2)), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Stopping fusion", ); diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index c36e88bbf..8fa0eedc6 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -225,6 +225,7 @@ class _Step4ViewState extends ConsumerState { builder: (context) { return BuildingTransactionDialog( coin: wallet.info.coin, + isSpark: wallet is FiroWallet && !firoPublicSend, onCancel: () { wasCancelled = true; }, @@ -249,7 +250,7 @@ class _Step4ViewState extends ConsumerState { address: address, amount: amount, isChange: false, - ) + ), ], note: "${model.trade!.payInCurrency.toUpperCase()}/" "${model.trade!.payOutCurrency.toUpperCase()} exchange", @@ -472,10 +473,10 @@ class _Step4ViewState extends ConsumerState { GestureDetector( onTap: () async { final data = ClipboardData( - text: - model.sendAmount.toString()); + text: model.sendAmount.toString(), + ); await clipboard.setData(data); - if (mounted) { + if (context.mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.info, @@ -535,9 +536,10 @@ class _Step4ViewState extends ConsumerState { GestureDetector( onTap: () async { final data = ClipboardData( - text: model.trade!.payInAddress); + text: model.trade!.payInAddress, + ); await clipboard.setData(data); - if (mounted) { + if (context.mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.info, @@ -598,10 +600,10 @@ class _Step4ViewState extends ConsumerState { GestureDetector( onTap: () async { final data = ClipboardData( - text: - model.trade!.payInExtraId); + text: model.trade!.payInExtraId, + ); await clipboard.setData(data); - if (mounted) { + if (context.mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.info, @@ -670,9 +672,10 @@ class _Step4ViewState extends ConsumerState { GestureDetector( onTap: () async { final data = ClipboardData( - text: model.trade!.tradeId); + text: model.trade!.tradeId, + ); await clipboard.setData(data); - if (mounted) { + if (context.mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.info, @@ -689,9 +692,9 @@ class _Step4ViewState extends ConsumerState { .infoItemIcons, width: 12, ), - ) + ), ], - ) + ), ], ), ), @@ -739,7 +742,8 @@ class _Step4ViewState extends ConsumerState { child: Text( "Send ${model.sendTicker} to this address", style: STextStyles.pageTitleH2( - context), + context, + ), ), ), const SizedBox( @@ -773,12 +777,13 @@ class _Step4ViewState extends ConsumerState { style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle( - context), + context, + ), child: Text( "Cancel", style: STextStyles.button( - context) - .copyWith( + context, + ).copyWith( color: Theme.of(context) .extension< StackColors>()! @@ -788,7 +793,7 @@ class _Step4ViewState extends ConsumerState { ), ), ], - ) + ), ], ), ); @@ -814,8 +819,9 @@ class _Step4ViewState extends ConsumerState { final tuple = ref .read( - exchangeSendFromWalletIdStateProvider - .state) + exchangeSendFromWalletIdStateProvider + .state, + ) .state; if (tuple != null && model.sendTicker.toLowerCase() == @@ -845,8 +851,8 @@ class _Step4ViewState extends ConsumerState { (BuildContext context) { final coin = coinFromTickerCaseInsensitive( - model.trade! - .payInCurrency); + model.trade!.payInCurrency, + ); return SendFromView( coin: coin, amount: model.sendAmount @@ -868,7 +874,8 @@ class _Step4ViewState extends ConsumerState { style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle( - context), + context, + ), child: Text( buttonTitle, style: diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 655f2afe7..a97b94a28 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -205,13 +205,13 @@ class _SendFromViewState extends ConsumerState { class SendFromCard extends ConsumerStatefulWidget { const SendFromCard({ - Key? key, + super.key, required this.walletId, required this.amount, required this.address, required this.trade, this.fromDesktopStep4 = false, - }) : super(key: key); + }); final String walletId; final Amount amount; @@ -235,6 +235,8 @@ class _SendFromCardState extends ConsumerState { try { bool wasCancelled = false; + final wallet = ref.read(pWallets).getWallet(walletId); + unawaited( showDialog( context: context, @@ -253,6 +255,8 @@ class _SendFromCardState extends ConsumerState { ), child: BuildingTransactionDialog( coin: coin, + isSpark: + wallet is FiroWallet && shouldSendPublicFiroFunds != true, onCancel: () { wasCancelled = true; @@ -273,8 +277,6 @@ class _SendFromCardState extends ConsumerState { TxData txData; Future txDataFuture; - final wallet = ref.read(pWallets).getWallet(walletId); - // if not firo then do normal send if (shouldSendPublicFiroFunds == null) { final memo = coin == Coin.stellar || coin == Coin.stellarTestnet @@ -371,38 +373,38 @@ class _SendFromCardState extends ConsumerState { } } } catch (e) { - // if (mounted) { - // pop building dialog - Navigator.of(context).pop(); + if (mounted) { + // pop building dialog + Navigator.of(context).pop(); - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Transaction failed", - message: e.toString(), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Ok", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), ), + onPressed: () { + Navigator.of(context).pop(); + }, ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - }, - ); - // } + ); + }, + ); + } } } @@ -420,7 +422,8 @@ class _SendFromCardState extends ConsumerState { final wallet = ref.watch(pWallets).getWallet(walletId); final locale = ref.watch( - localeServiceChangeNotifierProvider.select((value) => value.locale)); + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); final coin = ref.watch(pWalletCoin(walletId)); @@ -483,9 +486,11 @@ class _SendFromCardState extends ConsumerState { style: STextStyles.itemSubtitle(context), ), Text( - ref.watch(pAmountFormatter(coin)).format(ref - .watch(pWalletBalanceTertiary(walletId)) - .spendable), + ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pWalletBalanceTertiary(walletId)) + .spendable, + ), style: STextStyles.itemSubtitle(context), ), ], @@ -637,7 +642,8 @@ class _SendFromCardState extends ConsumerState { if (!isFiro) Text( ref.watch(pAmountFormatter(coin)).format( - ref.watch(pWalletBalance(walletId)).spendable), + ref.watch(pWalletBalance(walletId)).spendable, + ), style: STextStyles.itemSubtitle(context), ), ], diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index a747011ee..9e8bdb090 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -349,7 +349,7 @@ class _MonkeyViewState extends ConsumerState { ), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Saving MonKey svg", onException: (e) { didError = true; @@ -402,7 +402,7 @@ class _MonkeyViewState extends ConsumerState { const Duration(seconds: 2)), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Downloading MonKey png", onException: (e) { didError = true; @@ -500,7 +500,7 @@ class _MonkeyViewState extends ConsumerState { Future.delayed(const Duration(seconds: 2)), ]), context: context, - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, message: "Fetching MonKey", subMessage: "We are fetching your MonKey", onException: (e) { diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index d15523e10..590bca266 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -321,7 +321,7 @@ class _OrdinalImageGroup extends ConsumerWidget { final filePath = await showLoading( whileFuture: _savePngToFile(ref), context: context, - isDesktop: true, + rootNavigator: true, message: "Saving ordinal image", onException: (e) { didError = true; diff --git a/lib/pages/ordinals/ordinals_filter_view.dart b/lib/pages/ordinals/ordinals_filter_view.dart index 631a9833a..1d2f61695 100644 --- a/lib/pages/ordinals/ordinals_filter_view.dart +++ b/lib/pages/ordinals/ordinals_filter_view.dart @@ -10,7 +10,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/theme_providers.dart'; @@ -21,6 +20,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/date_picker/date_picker.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -69,8 +69,8 @@ final ordinalFilterProvider = StateProvider((_) => null); class OrdinalsFilterView extends ConsumerStatefulWidget { const OrdinalsFilterView({ - Key? key, - }) : super(key: key); + super.key, + }); static const String routeName = "/ordinalsFilterView"; @@ -146,56 +146,6 @@ class _OrdinalsFilterViewState extends ConsumerState { DateTime? _selectedFromDate = DateTime(2007); DateTime? _selectedToDate = DateTime.now(); - MaterialRoundedDatePickerStyle _buildDatePickerStyle() { - return MaterialRoundedDatePickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - // backgroundHeader: Theme.of(context).extension()!.textSubtitle2, - paddingMonthHeader: const EdgeInsets.only(top: 11), - colorArrowNext: Theme.of(context).extension()!.textSubtitle1, - colorArrowPrevious: - Theme.of(context).extension()!.textSubtitle1, - textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), - textStyleDayHeader: STextStyles.datePicker600(context), - textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( - color: baseColor, - ), - textStyleDayOnCalendarDisabled: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle3, - ), - textStyleDayOnCalendarSelected: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textStyleYearButton: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - // textStyleButtonAction: GoogleFonts.inter(), - ); - } - - MaterialRoundedYearPickerStyle _buildYearPickerStyle() { - return MaterialRoundedYearPickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - textStyleYear: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle2, - fontSize: 16, - ), - textStyleYearSelected: STextStyles.datePicker600(context).copyWith( - fontSize: 18, - ), - ); - } - Widget _buildDateRangePicker() { const middleSeparatorPadding = 2.0; const middleSeparatorWidth = 12.0; @@ -216,9 +166,6 @@ class _OrdinalsFilterViewState extends ConsumerState { child: GestureDetector( key: const Key("OrdinalsViewFromDatePickerKey"), onTap: () async { - final color = - Theme.of(context).extension()!.accentColorDark; - final height = MediaQuery.of(context).size.height; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); @@ -226,28 +173,7 @@ class _OrdinalsFilterViewState extends ConsumerState { } if (mounted) { - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - initialDate: DateTime.now(), - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); + final date = await showSWDatePicker(context); if (date != null) { _selectedFromDate = date; @@ -330,9 +256,6 @@ class _OrdinalsFilterViewState extends ConsumerState { child: GestureDetector( key: const Key("OrdinalsViewToDatePickerKey"), onTap: () async { - final color = - Theme.of(context).extension()!.accentColorDark; - final height = MediaQuery.of(context).size.height; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); @@ -340,28 +263,7 @@ class _OrdinalsFilterViewState extends ConsumerState { } if (mounted) { - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - initialDate: DateTime.now(), - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, - - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); + final date = await showSWDatePicker(context); if (date != null) { _selectedToDate = date; @@ -467,7 +369,7 @@ class _OrdinalsFilterViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 75)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -840,7 +742,7 @@ class _OrdinalsFilterViewState extends ConsumerState { ); } } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, diff --git a/lib/pages/paynym/subwidgets/paynym_bot.dart b/lib/pages/paynym/subwidgets/paynym_bot.dart index d8f645da3..082f7034e 100644 --- a/lib/pages/paynym/subwidgets/paynym_bot.dart +++ b/lib/pages/paynym/subwidgets/paynym_bot.dart @@ -8,8 +8,12 @@ * */ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/networking/http.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/prefs.dart'; class PayNymBot extends StatelessWidget { const PayNymBot({ @@ -28,16 +32,37 @@ class PayNymBot extends StatelessWidget { child: SizedBox( width: size, height: size, - child: Image.network( - "https://paynym.is/$paymentCodeString/avatar", - loadingBuilder: (context, child, loadingProgress) => - loadingProgress == null - ? child - : const Center( - child: LoadingIndicator(), - ), + child: FutureBuilder( + future: _fetchImage(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Image.memory(snapshot.data!); + } else if (snapshot.hasError) { + return const Center(child: Icon(Icons.error)); + } else { + return const Center(); // TODO [prio=low]: Make better loading indicator. + } + }, ), ), ); } + + Future _fetchImage() async { + final HTTP client = HTTP(); + final Uri uri = Uri.parse("https://paynym.is/$paymentCodeString/avatar"); + + final response = await client.get( + url: uri, + proxyInfo: Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + if (response.code == 200) { + return Uint8List.fromList(response.bodyBytes); + } else { + throw Exception('Failed to load image'); + } + } } diff --git a/lib/pages/receive_view/addresses/address_card.dart b/lib/pages/receive_view/addresses/address_card.dart index 05c645352..51e1402a3 100644 --- a/lib/pages/receive_view/addresses/address_card.dart +++ b/lib/pages/receive_view/addresses/address_card.dart @@ -10,31 +10,46 @@ import 'dart:async'; import 'dart:io'; +import 'dart:ui' as ui; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar/isar.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/pages/receive_view/addresses/address_tag.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_edit_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; class AddressCard extends ConsumerStatefulWidget { const AddressCard({ - Key? key, + super.key, required this.addressId, required this.walletId, required this.coin, this.onPressed, this.clipboard = const ClipboardWrapper(), - }) : super(key: key); + }); final int addressId; final String walletId; @@ -47,6 +62,7 @@ class AddressCard extends ConsumerStatefulWidget { } class _AddressCardState extends ConsumerState { + final _qrKey = GlobalKey(); final isDesktop = Util.isDesktop; late Stream stream; @@ -54,6 +70,72 @@ class _AddressCardState extends ConsumerState { AddressLabel? label; + Future _capturePng(bool shouldSaveInsteadOfShare) async { + try { + final RenderRepaintBoundary boundary = + _qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary; + final ui.Image image = await boundary.toImage(); + final ByteData? byteData = + await image.toByteData(format: ui.ImageByteFormat.png); + final Uint8List pngBytes = byteData!.buffer.asUint8List(); + + if (shouldSaveInsteadOfShare) { + if (Util.isDesktop) { + final dir = Directory("${Platform.environment['HOME']}"); + if (!dir.existsSync()) { + throw Exception( + "Home dir not found while trying to open filepicker on QR image save"); + } + final path = await FilePicker.platform.saveFile( + fileName: "qrcode.png", + initialDirectory: dir.path, + ); + + if (path != null) { + final file = File(path); + if (file.existsSync()) { + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$path already exists!", + context: context, + ), + ); + } + } else { + await file.writeAsBytes(pngBytes); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "$path saved!", + context: context, + ), + ); + } + } + } + } else { + // await DocumentFileSavePlus.saveFile( + // pngBytes, + // "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png", + // "image/png"); + } + } else { + final tempDir = await getTemporaryDirectory(); + final file = await File("${tempDir.path}/qrcode.png").create(); + await file.writeAsBytes(pngBytes); + + await Share.shareFiles(["${tempDir.path}/qrcode.png"], + text: "Receive URI QR Code"); + } + } catch (e) { + //todo: comeback to this + debugPrint(e.toString()); + } + } + @override void initState() { address = MainDB.instance.isar.addresses @@ -117,16 +199,32 @@ class _AddressCardState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (label!.value.isNotEmpty) - Text( - label!.value, - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ), - if (label!.value.isNotEmpty) - SizedBox( - height: isDesktop ? 2 : 8, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label!.value.isNotEmpty ? label!.value : "No label", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + SimpleEditButton( + editValue: label!.value, + editLabel: 'label', + overrideTitle: 'Edit label', + disableIcon: true, + onValueChanged: (value) { + MainDB.instance.putAddressLabel( + label!.copyWith( + label: value, + ), + ); + }, + ), + ], + ), + SizedBox( + height: isDesktop ? 2 : 8, + ), Row( children: [ Expanded( @@ -140,18 +238,152 @@ class _AddressCardState extends ConsumerState { const SizedBox( height: 10, ), - if (label!.tags != null && label!.tags!.isNotEmpty) - Wrap( - spacing: 10, - runSpacing: 10, - children: label!.tags! - .map( - (e) => AddressTag( - tag: e, + Row( + children: [ + CustomTextButton( + text: "Copy address", + onTap: () { + widget.clipboard + .setData( + ClipboardData( + text: address.value, ), ) - .toList(), - ), + .then((value) { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }); + }, + ), + const SizedBox( + width: 16, + ), + CustomTextButton( + text: "Show QR code", + onTap: () async { + await showDialog( + context: context, + builder: (_) { + return StackDialogBase( + child: Column( + children: [ + if (label!.value.isNotEmpty) + Text( + label!.value, + style: STextStyles.w600_18(context), + ), + if (label!.value.isNotEmpty) + const SizedBox( + height: 8, + ), + Text( + address.value, + style: + STextStyles.w500_16(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 16, + ), + Center( + child: RepaintBoundary( + key: _qrKey, + child: QrImageView( + data: AddressUtils.buildUriString( + widget.coin, + address.value, + {}, + ), + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .popupBG, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + if (!isDesktop) + Expanded( + child: SecondaryButton( + label: "Share", + buttonHeight: isDesktop + ? ButtonHeight.l + : null, + icon: SvgPicture.asset( + Assets.svg.share, + width: 14, + height: 14, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + await _capturePng(false); + }, + ), + ), + if (isDesktop) + Expanded( + child: PrimaryButton( + buttonHeight: isDesktop + ? ButtonHeight.l + : null, + onPressed: () async { + // TODO: add save functionality instead of share + // save works on linux at the moment + await _capturePng(true); + }, + label: "Save", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + ), + ), + ], + ) + ], + ), + ); + }, + ); + }, + ), + ], + ), + // if (label!.tags != null && label!.tags!.isNotEmpty) + // Wrap( + // spacing: 10, + // runSpacing: 10, + // children: label!.tags! + // .map( + // (e) => AddressTag( + // tag: e, + // ), + // ) + // .toList(), + // ), ], ), ); diff --git a/lib/pages/receive_view/addresses/wallet_addresses_view.dart b/lib/pages/receive_view/addresses/wallet_addresses_view.dart index 597ca44fe..c6cd03215 100644 --- a/lib/pages/receive_view/addresses/wallet_addresses_view.dart +++ b/lib/pages/receive_view/addresses/wallet_addresses_view.dart @@ -10,14 +10,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_details_view.dart'; import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; @@ -25,13 +23,8 @@ import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; -import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:tuple/tuple.dart'; -import '../../../utilities/assets.dart'; -import '../../../widgets/icon_widgets/x_icon.dart'; -import '../../../widgets/textfield_icon_button.dart'; - class WalletAddressesView extends ConsumerStatefulWidget { const WalletAddressesView({ Key? key, @@ -50,10 +43,10 @@ class WalletAddressesView extends ConsumerStatefulWidget { class _WalletAddressesViewState extends ConsumerState { final bool isDesktop = Util.isDesktop; - String _searchString = ""; + final String _searchString = ""; - late final TextEditingController _searchController; - final searchFieldFocusNode = FocusNode(); + // late final TextEditingController _searchController; + // final searchFieldFocusNode = FocusNode(); Future> _search(String term) async { if (term.isEmpty) { @@ -119,19 +112,19 @@ class _WalletAddressesViewState extends ConsumerState { .findAll(); } - @override - void initState() { - _searchController = TextEditingController(); - - super.initState(); - } - - @override - void dispose() { - _searchController.dispose(); - searchFieldFocusNode.dispose(); - super.dispose(); - } + // @override + // void initState() { + // _searchController = TextEditingController(); + // + // super.initState(); + // } + // + // @override + // void dispose() { + // _searchController.dispose(); + // searchFieldFocusNode.dispose(); + // super.dispose(); + // } @override Widget build(BuildContext context) { @@ -165,74 +158,74 @@ class _WalletAddressesViewState extends ConsumerState { ), child: Column( children: [ - SizedBox( - width: isDesktop ? 490 : null, - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: _searchController, - focusNode: searchFieldFocusNode, - onChanged: (value) { - setState(() { - _searchString = value; - }); - }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: standardInputDecoration( - "Search...", - searchFieldFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 12 : 10, - vertical: isDesktop ? 18 : 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: isDesktop ? 20 : 16, - height: isDesktop ? 20 : 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - ), - SizedBox( - height: isDesktop ? 20 : 16, - ), + // SizedBox( + // width: isDesktop ? 490 : null, + // child: ClipRRect( + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // child: TextField( + // autocorrect: !isDesktop, + // enableSuggestions: !isDesktop, + // controller: _searchController, + // focusNode: searchFieldFocusNode, + // onChanged: (value) { + // setState(() { + // _searchString = value; + // }); + // }, + // style: isDesktop + // ? STextStyles.desktopTextExtraSmall(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .textFieldActiveText, + // height: 1.8, + // ) + // : STextStyles.field(context), + // decoration: standardInputDecoration( + // "Search...", + // searchFieldFocusNode, + // context, + // desktopMed: isDesktop, + // ).copyWith( + // prefixIcon: Padding( + // padding: EdgeInsets.symmetric( + // horizontal: isDesktop ? 12 : 10, + // vertical: isDesktop ? 18 : 16, + // ), + // child: SvgPicture.asset( + // Assets.svg.search, + // width: isDesktop ? 20 : 16, + // height: isDesktop ? 20 : 16, + // ), + // ), + // suffixIcon: _searchController.text.isNotEmpty + // ? Padding( + // padding: const EdgeInsets.only(right: 0), + // child: UnconstrainedBox( + // child: Row( + // children: [ + // TextFieldIconButton( + // child: const XIcon(), + // onTap: () async { + // setState(() { + // _searchController.text = ""; + // _searchString = ""; + // }); + // }, + // ), + // ], + // ), + // ), + // ) + // : null, + // ), + // ), + // ), + // ), + // SizedBox( + // height: isDesktop ? 20 : 16, + // ), Expanded( child: FutureBuilder( future: _search(_searchString), @@ -249,15 +242,17 @@ class _WalletAddressesViewState extends ConsumerState { walletId: widget.walletId, addressId: snapshot.data![index], coin: coin, - onPressed: () { - Navigator.of(context).pushNamed( - AddressDetailsView.routeName, - arguments: Tuple2( - snapshot.data![index], - widget.walletId, - ), - ); - }, + onPressed: !isDesktop + ? null + : () { + Navigator.of(context).pushNamed( + AddressDetailsView.routeName, + arguments: Tuple2( + snapshot.data![index], + widget.walletId, + ), + ); + }, ), ); } else { diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 56a341f5d..f96e83274 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -30,8 +30,12 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -39,15 +43,17 @@ import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class ReceiveView extends ConsumerStatefulWidget { const ReceiveView({ - Key? key, + super.key, required this.walletId, this.tokenContract, this.clipboard = const ClipboardWrapper(), - }) : super(key: key); + }); static const String routeName = "/receiveView"; @@ -63,11 +69,14 @@ class _ReceiveViewState extends ConsumerState { late final Coin coin; late final String walletId; late final ClipboardInterface clipboard; - late final bool supportsSpark; + late final bool _supportsSpark; + late final bool _showMultiType; - String? _sparkAddress; - String? _qrcodeContent; - bool _showSparkAddress = true; + int _currentIndex = 0; + + final List _walletAddressTypes = []; + final Map _addressMap = {}; + final Map> _addressSubMap = {}; Future generateNewAddress() async { final wallet = ref.read(pWallets).getWallet(walletId); @@ -95,13 +104,32 @@ class _ReceiveViewState extends ConsumerState { ), ); - await wallet.generateNewReceivingAddress(); + final Address? address; + if (wallet is Bip39HDWallet && wallet is! BCashInterface) { + final type = DerivePathType.values.firstWhere( + (e) => e.getAddressType() == _walletAddressTypes[_currentIndex], + ); + address = await wallet.generateNextReceivingAddress( + derivePathType: type, + ); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address!); + }); + } else { + await wallet.generateNewReceivingAddress(); + address = null; + } shouldPop = true; if (mounted) { Navigator.of(context) .popUntil(ModalRoute.withName(ReceiveView.routeName)); + + setState(() { + _addressMap[_walletAddressTypes[_currentIndex]] = + address?.value ?? ref.read(pWalletReceivingAddress(walletId)); + }); } } } @@ -140,45 +168,68 @@ class _ReceiveViewState extends ConsumerState { if (mounted) { Navigator.of(context, rootNavigator: true).pop(); - if (_sparkAddress != address.value) { - setState(() { - _sparkAddress = address.value; - }); - } + setState(() { + _addressMap[AddressType.spark] = address.value; + }); } } } - StreamSubscription? _streamSub; - @override void initState() { walletId = widget.walletId; coin = ref.read(pWalletCoin(walletId)); clipboard = widget.clipboard; - supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; + final wallet = ref.read(pWallets).getWallet(walletId); + _supportsSpark = wallet is SparkInterface; + _showMultiType = _supportsSpark || + (wallet is! BCashInterface && + wallet is Bip39HDWallet && + wallet.supportedAddressTypes.length > 1); - if (supportsSpark) { - _streamSub = ref - .read(mainDBProvider) - .isar - .addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .typeEqualTo(AddressType.spark) - .sortByDerivationIndexDesc() - .findFirst() - .asStream() - .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _sparkAddress = event?.value; - }); - } + _walletAddressTypes.add(coin.primaryAddressType); + + if (_showMultiType) { + if (_supportsSpark) { + _walletAddressTypes.insert(0, AddressType.spark); + } else { + _walletAddressTypes.addAll((wallet as Bip39HDWallet) + .supportedAddressTypes + .where((e) => e != coin.primaryAddressType)); + } + } + + if (_walletAddressTypes.length > 1 && wallet is BitcoinWallet) { + _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); + } + + _addressMap[_walletAddressTypes[_currentIndex]] = + ref.read(pWalletReceivingAddress(walletId)); + + if (_showMultiType) { + for (final type in _walletAddressTypes) { + _addressSubMap[type] = ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(type) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[type] = + event?.value ?? _addressMap[type] ?? "[No address yet]"; + }); + } + }); }); - }); + } } super.initState(); @@ -186,7 +237,9 @@ class _ReceiveViewState extends ConsumerState { @override void dispose() { - _streamSub?.cancel(); + for (final subscription in _addressSubMap.values) { + subscription.cancel(); + } super.dispose(); } @@ -196,14 +249,11 @@ class _ReceiveViewState extends ConsumerState { final ticker = widget.tokenContract?.symbol ?? coin.ticker; - if (supportsSpark) { - if (_showSparkAddress) { - _qrcodeContent = _sparkAddress; - } else { - _qrcodeContent = ref.watch(pWalletReceivingAddress(walletId)); - } + final String address; + if (_showMultiType) { + address = _addressMap[_walletAddressTypes[_currentIndex]]!; } else { - _qrcodeContent = ref.watch(pWalletReceivingAddress(walletId)); + address = ref.watch(pWalletReceivingAddress(walletId)); } return Background( @@ -319,33 +369,44 @@ class _ReceiveViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ConditionalParent( - condition: supportsSpark, + condition: _showMultiType, builder: (child) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Text( + "Address type", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ), + ), + const SizedBox( + height: 10, + ), DropdownButtonHideUnderline( - child: DropdownButton2( - value: _showSparkAddress, + child: DropdownButton2( + value: _currentIndex, items: [ - DropdownMenuItem( - value: true, - child: Text( - "Spark address", - style: STextStyles.desktopTextMedium(context), + for (int i = 0; + i < _walletAddressTypes.length; + i++) + DropdownMenuItem( + value: i, + child: Text( + _supportsSpark && + _walletAddressTypes[i] == + AddressType.p2pkh + ? "Transparent address" + : "${_walletAddressTypes[i].readableName} address", + style: STextStyles.w500_14(context), + ), ), - ), - DropdownMenuItem( - value: false, - child: Text( - "Transparent address", - style: STextStyles.desktopTextMedium(context), - ), - ), ], onChanged: (value) { - if (value is bool && value != _showSparkAddress) { + if (value != null && value != _currentIndex) { setState(() { - _showSparkAddress = value; + _currentIndex = value; }); } }, @@ -363,6 +424,16 @@ class _ReceiveViewState extends ConsumerState { ), ), ), + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, @@ -386,89 +457,7 @@ class _ReceiveViewState extends ConsumerState { const SizedBox( height: 12, ), - if (_showSparkAddress) - GestureDetector( - onTap: () { - clipboard.setData( - ClipboardData(text: _sparkAddress ?? "Error"), - ); - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - width: 1, - ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: RoundedWhiteContainer( - child: Column( - children: [ - Row( - children: [ - Text( - "Your ${coin.ticker} SPARK address", - style: - STextStyles.itemSubtitle(context), - ), - const Spacer(), - Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 15, - height: 15, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), - ], - ), - const SizedBox( - height: 8, - ), - Row( - children: [ - Expanded( - child: Text( - _sparkAddress ?? "Error", - style: STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - if (!_showSparkAddress) child, + child, ], ), child: GestureDetector( @@ -476,8 +465,8 @@ class _ReceiveViewState extends ConsumerState { HapticFeedback.lightImpact(); clipboard.setData( ClipboardData( - text: - ref.watch(pWalletReceivingAddress(walletId))), + text: address, + ), ); showFloatingFlushBar( type: FlushBarType.info, @@ -524,8 +513,7 @@ class _ReceiveViewState extends ConsumerState { children: [ Expanded( child: Text( - ref.watch( - pWalletReceivingAddress(walletId)), + address, style: STextStyles.itemSubtitle12(context), ), ), @@ -536,31 +524,44 @@ class _ReceiveViewState extends ConsumerState { ), ), ), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Copy address", + onPressed: () { + HapticFeedback.lightImpact(); + clipboard.setData( + ClipboardData( + text: address, + ), + ); + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + }, + ), if (ref.watch(pWallets .select((value) => value.getWallet(walletId))) is MultiAddressInterface || - supportsSpark) + _supportsSpark) const SizedBox( height: 12, ), if (ref.watch(pWallets .select((value) => value.getWallet(walletId))) is MultiAddressInterface || - supportsSpark) - TextButton( - onPressed: supportsSpark && _showSparkAddress + _supportsSpark) + SecondaryButton( + label: "Generate new address", + onPressed: _supportsSpark && + _walletAddressTypes[_currentIndex] == + AddressType.spark ? generateNewSparkAddress : generateNewAddress, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Generate new address", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), ), const SizedBox( height: 30, @@ -574,7 +575,7 @@ class _ReceiveViewState extends ConsumerState { QrImageView( data: AddressUtils.buildUriString( coin, - _qrcodeContent ?? "", + address, {}, ), size: MediaQuery.of(context).size.width / 2, @@ -585,7 +586,7 @@ class _ReceiveViewState extends ConsumerState { height: 20, ), CustomTextButton( - text: "Create new QR code", + text: "Advanced options", onTap: () async { unawaited(Navigator.of(context).push( RouteGenerator.getRoute( @@ -593,7 +594,7 @@ class _ReceiveViewState extends ConsumerState { RouteGenerator.useMaterialPageRoute, builder: (_) => GenerateUriQrCodeView( coin: coin, - receivingAddress: _qrcodeContent ?? "", + receivingAddress: address, ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart new file mode 100644 index 000000000..ccaf5452e --- /dev/null +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -0,0 +1,572 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/recipient.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/themes/coin_icon_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/amount/amount_formatter.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/fee_slider.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:tuple/tuple.dart'; + +class FrostSendView extends ConsumerStatefulWidget { + const FrostSendView({ + super.key, + required this.walletId, + required this.coin, + }); + + static const String routeName = "/frostSendView"; + + final String walletId; + final Coin coin; + + @override + ConsumerState createState() => _FrostSendViewState(); +} + +class _FrostSendViewState extends ConsumerState { + final List recipientWidgetIndexes = [0]; + int _greatestWidgetIndex = 0; + + late final String walletId; + late final Coin coin; + + late TextEditingController noteController; + late TextEditingController onChainNoteController; + + final _noteFocusNode = FocusNode(); + + Set selectedUTXOs = {}; + + bool _createSignLock = false; + + Future _loadingFuture() async { + final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet; + + final recipients = recipientWidgetIndexes + .map((i) => ref.read(pRecipient(i).state).state) + .map((e) => (address: e!.address, amount: e!.amount!, isChange: false)) + .toList(growable: false); + + final txData = await wallet.frostCreateSignConfig( + txData: TxData(recipients: recipients), + changeAddress: (await wallet.getCurrentReceivingAddress())!.value, + feePerWeight: customFeeRate, + ); + + return txData; + } + + Future _createSignConfig() async { + if (_createSignLock) { + return; + } + _createSignLock = true; + + try { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + + TxData? txData; + if (mounted) { + txData = await showLoading( + whileFuture: _loadingFuture(), + context: context, + message: "Generating sign config", + rootNavigator: Util.isDesktop, + onException: (e) { + throw e; + }, + ); + } + + final wallet = + ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet; + + if (mounted && txData != null) { + ref.read(pFrostTxData.notifier).state = txData; + + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: walletId, + stepRoutes: FrostRouteGenerator.sendFrostTxStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.transactionCreation, + callerRouteName: FrostSendView.routeName, + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + } + } catch (e) { + if (mounted) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Create sign config failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + ), + ); + } + } finally { + _createSignLock = false; + } + } + + int customFeeRate = 1; + + bool _buttonEnabled = false; + + bool _validateRecipientFormStatesHelper() { + for (final i in recipientWidgetIndexes) { + final state = ref.read(pRecipient(i)); + if (state?.amount == null || + state?.address == null || + state!.address.isEmpty) { + return false; + } + } + return true; + } + + void _validateRecipientFormStates() { + setState(() { + _buttonEnabled = _validateRecipientFormStatesHelper(); + }); + } + + @override + void initState() { + coin = widget.coin; + walletId = widget.walletId; + + noteController = TextEditingController(); + + super.initState(); + } + + @override + void dispose() { + noteController.dispose(); + + _noteFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final wallet = ref.watch(pWallets).getWallet(walletId); + + final showCoinControl = wallet is CoinControlInterface && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, + ), + ); + + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 50)); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Send ${coin.ticker}", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: child, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!Util.isDesktop) + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch( + coinIconProvider(coin), + ), + ), + width: 22, + height: 22, + ), + const SizedBox( + width: 6, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ref.watch(pWalletName(walletId)), + style: STextStyles.titleBold12(context) + .copyWith(fontSize: 14), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + // const SizedBox( + // height: 2, + // ), + Text( + "Available balance", + style: STextStyles.label(context) + .copyWith(fontSize: 10), + ), + ], + ), + Util.isDesktop + ? const SizedBox( + height: 24, + ) + : const Spacer(), + GestureDetector( + onTap: () {}, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + ref.watch(pAmountFormatter(coin)).format(ref + .watch(pWalletBalance(walletId)) + .spendable), + style: + STextStyles.titleBold12(context).copyWith( + fontSize: 10, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + ) + ], + ), + ), + ), + SizedBox( + height: recipientWidgetIndexes.length > 1 ? 8 : 16, + ), + Column( + children: [ + for (int i = 0; i < recipientWidgetIndexes.length; i++) + ConditionalParent( + condition: recipientWidgetIndexes.length > 1, + builder: (child) => Padding( + padding: const EdgeInsets.only(top: 8), + child: child, + ), + child: Recipient( + key: Key( + "recipientKey_${recipientWidgetIndexes[i]}", + ), + index: recipientWidgetIndexes[i], + displayNumber: i + 1, + coin: coin, + onChanged: () { + _validateRecipientFormStates(); + }, + remove: i == 0 && recipientWidgetIndexes.length == 1 + ? null + : () { + ref + .read(pRecipient(recipientWidgetIndexes[i]) + .notifier) + .state = null; + recipientWidgetIndexes.removeAt(i); + setState(() {}); + _validateRecipientFormStates(); + }, + addAnotherRecipientTapped: () { + // used for tracking recipient forms + _greatestWidgetIndex++; + recipientWidgetIndexes.add(_greatestWidgetIndex); + setState(() {}); + _validateRecipientFormStates(); + }, + sendAllTapped: () { + return ref.read(pAmountFormatter(coin)).format( + ref.read(pWalletBalance(walletId)).spendable, + withUnitName: false, + ); + }, + ), + ), + ], + ), + if (recipientWidgetIndexes.length > 1) + const SizedBox( + height: 12, + ), + if (recipientWidgetIndexes.length > 1) + SecondaryButton( + width: double.infinity, + label: "Add recipient", + onPressed: () { + // used for tracking recipient forms + _greatestWidgetIndex++; + recipientWidgetIndexes.add(_greatestWidgetIndex); + setState(() {}); + }, + ), + if (showCoinControl) + const SizedBox( + height: 8, + ), + if (showCoinControl) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Coin control", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + CustomTextButton( + text: selectedUTXOs.isEmpty + ? "Select coins" + : "Selected coins (${selectedUTXOs.length})", + onTap: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + } + + if (mounted) { + // TODO: [prio=high] make sure this coincontrol works correctly + + Amount? amount; + + final result = await Navigator.of(context).pushNamed( + CoinControlView.routeName, + arguments: Tuple4( + walletId, + CoinControlViewType.use, + amount, + selectedUTXOs, + ), + ); + + if (result is Set) { + setState(() { + selectedUTXOs = result; + }); + } + } + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + bottom: 12, + top: 16, + ), + child: FeeSlider( + coin: coin, + showWU: true, + onSatVByteChanged: (rate) { + customFeeRate = rate; + }, + ), + ), + Util.isDesktop + ? const SizedBox( + height: 12, + ) + : const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Create multisig transaction", + enabled: _buttonEnabled, + onPressed: _createSignConfig, + ), + const SizedBox( + height: 16, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart new file mode 100644 index 000000000..043f080f6 --- /dev/null +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -0,0 +1,447 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/amount/amount_formatter.dart'; +import 'package:stackwallet/utilities/amount/amount_input_formatter.dart'; +import 'package:stackwallet/utilities/amount/amount_unit.dart'; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +//TODO: move the following two providers elsewhere +final pClipboard = + Provider((ref) => const ClipboardWrapper()); +final pBarcodeScanner = + Provider((ref) => const BarcodeScannerWrapper()); + +// final _pPrice = Provider.family((ref, coin) { +// return ref.watch( +// priceAnd24hChangeNotifierProvider +// .select((value) => value.getPrice(coin).item1), +// ); +// }); + +final pRecipient = + StateProvider.family<({String address, Amount? amount})?, int>( + (ref, index) => null); + +class Recipient extends ConsumerStatefulWidget { + const Recipient({ + super.key, + required this.index, + required this.displayNumber, + required this.coin, + this.remove, + this.onChanged, + required this.addAnotherRecipientTapped, + required this.sendAllTapped, + }); + + final int index; + final int displayNumber; + final Coin coin; + + final VoidCallback? remove; + final VoidCallback? onChanged; + final VoidCallback addAnotherRecipientTapped; + final String Function() sendAllTapped; + + @override + ConsumerState createState() => _RecipientState(); +} + +class _RecipientState extends ConsumerState { + late final TextEditingController addressController, amountController; + late final FocusNode addressFocusNode, amountFocusNode; + + bool _addressIsEmpty = true; + final bool _cryptoAmountChangeLock = false; + + bool get isSingle => widget.remove == null; + + void _updateRecipientData() { + final address = addressController.text; + final amount = + ref.read(pAmountFormatter(widget.coin)).tryParse(amountController.text); + + ref.read(pRecipient(widget.index).notifier).state = ( + address: address, + amount: amount, + ); + widget.onChanged?.call(); + } + + void _cryptoAmountChanged() async { + if (!_cryptoAmountChangeLock) { + Amount? cryptoAmount = ref.read(pAmountFormatter(widget.coin)).tryParse( + amountController.text, + ); + if (cryptoAmount != null) { + if (ref.read(pRecipient(widget.index))?.amount != null && + ref.read(pRecipient(widget.index))?.amount == cryptoAmount) { + return; + } + + // final price = ref.read(_pPrice(widget.coin)); + // + // if (price > Decimal.zero) { + // baseController.text = (cryptoAmount.decimal * price) + // .toAmount( + // fractionDigits: 2, + // ) + // .fiatString( + // locale: ref.read(localeServiceChangeNotifierProvider).locale, + // ); + // } + } else { + cryptoAmount = null; + // baseController.text = ""; + } + + _updateRecipientData(); + } + } + + @override + void initState() { + addressController = TextEditingController(); + amountController = TextEditingController(); + // baseController = TextEditingController(); + + final amount = ref.read(pRecipient(widget.index))?.amount; + if (amount != null) { + amountController.text = ref + .read(pAmountFormatter(widget.coin)) + .format(amount, withUnitName: false); + } + addressController.text = ref.read(pRecipient(widget.index))?.address ?? ""; + + _addressIsEmpty = addressController.text.isEmpty; + + addressFocusNode = FocusNode(); + amountFocusNode = FocusNode(); + // baseFocusNode = FocusNode(); + + amountController.addListener(_cryptoAmountChanged); + + super.initState(); + } + + @override + void dispose() { + amountController.removeListener(_cryptoAmountChanged); + + addressController.dispose(); + amountController.dispose(); + // baseController.dispose(); + + addressFocusNode.dispose(); + amountFocusNode.dispose(); + // baseFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final String locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); + + return RoundedContainer( + color: Colors.transparent, + padding: const EdgeInsets.all(0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isSingle ? "Send to" : "Recipient ${widget.displayNumber}", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: isSingle ? "Add another recipient" : "Remove", + onTap: + isSingle ? widget.addAnotherRecipientTapped : widget.remove, + ), + ], + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("sendViewAddressFieldKey"), + controller: addressController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + focusNode: addressFocusNode, + style: STextStyles.field(context), + onChanged: (_) { + _updateRecipientData(); + setState(() { + _addressIsEmpty = addressController.text.isEmpty; + }); + }, + decoration: standardInputDecoration( + "Enter ${widget.coin.ticker} address", + addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _addressIsEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_addressIsEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "sendViewClearAddressFieldButtonKey"), + onTap: () { + addressController.text = ""; + + setState(() { + _addressIsEmpty = true; + }); + + _updateRecipientData(); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "sendViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await ref + .read(pClipboard) + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, content.indexOf("\n")); + } + + addressController.text = content.trim(); + + setState(() { + _addressIsEmpty = + addressController.text.isEmpty; + }); + + _updateRecipientData(); + } + }, + child: _addressIsEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_addressIsEmpty) + TextFieldIconButton( + semanticsLabel: "Scan QR Button. " + "Opens Camera For Scanning QR Code.", + key: const Key( + "sendViewScanQrButtonKey", + ), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75, + ), + ); + } + + final qrResult = + await ref.read(pBarcodeScanner).scan(); + + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); + + /// TODO: deal with address utils + final results = + AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log( + "qrResult parsed: $results", + level: LogLevel.Info, + ); + + if (results.isNotEmpty && + results["scheme"] == + widget.coin.uriScheme) { + // auto fill address + + addressController.text = + (results["address"] ?? "").trim(); + + // autofill amount field + if (results["amount"] != null) { + final Amount amount = + Decimal.parse(results["amount"]!) + .toAmount( + fractionDigits: widget.coin.decimals, + ); + amountController.text = ref + .read(pAmountFormatter(widget.coin)) + .format( + amount, + withUnitName: false, + ); + } + } else { + addressController.text = + qrResult.rawContent.trim(); + } + + setState(() { + _addressIsEmpty = + addressController.text.isEmpty; + }); + + _updateRecipientData(); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while " + "trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + SizedBox( + height: isSingle ? 12 : 8, + ), + if (isSingle) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + // disable send all since the frost tx creation logic isn't there (yet?) + const Spacer(), + // CustomTextButton( + // text: "Send all ${widget.coin.ticker}", + // onTap: () { + // amountController.text = widget.sendAllTapped(); + // _cryptoAmountChanged(); + // }, + // ), + ], + ), + if (isSingle) + const SizedBox( + height: 8, + ), + TextField( + autocorrect: false, + enableSuggestions: false, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + key: const Key("amountInputFieldCryptoTextFieldKey"), + controller: amountController, + focusNode: amountFocusNode, + onChanged: (_) { + _updateRecipientData(); + }, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + AmountInputFormatter( + decimals: widget.coin.decimals, + unit: ref.watch(pAmountUnit(widget.coin)), + locale: locale, + ), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref + .watch(pAmountUnit(widget.coin)) + .unitForCoin(widget.coin), + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart new file mode 100644 index 000000000..d49474f6d --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostSendStep1a extends ConsumerStatefulWidget { + const FrostSendStep1a({super.key}); + + static const String routeName = "/FrostSendStep1a"; + static const String title = "FROST transaction"; + + @override + ConsumerState createState() => _FrostSendStep1aState(); +} + +class _FrostSendStep1aState extends ConsumerState { + static const steps2to4 = [ + "Wait for them to import the transaction config.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be " + "canceled.", + "Check the box and press “Attempt sign”.", + ]; + + late final int _threshold; + + bool _userVerifyContinue = false; + bool _attemptSignLock = false; + + Future _attemptSign() async { + if (_attemptSignLock) { + return; + } + + _attemptSignLock = true; + + try { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + final attemptSignRes = await wallet.frostAttemptSignConfig( + config: ref.read(pFrostTxData.state).state!.frostMSConfig!, + ); + + ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Error, + ); + } finally { + _attemptSignLock = false; + } + } + + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + _threshold = wallet.frostInfo.threshold; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(pFrostMyName.state).state = wallet.frostInfo.myName; + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final double qrImageSize = + Util.isDesktop ? 360 : MediaQuery.of(context).size.width / 1.67; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "1.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: + "Share this config with the group members. ", + style: STextStyles.w500_12(context), + ), + TextSpan( + text: + "You must have the threshold number of signatures (including yours) to send the transaction.", + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], + ), + ), + ), + ], + ), + for (int i = 0; i < steps2to4.length; i++) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${i + 2}.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + steps2to4[i], + style: STextStyles.w500_12(context), + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + SizedBox( + height: qrImageSize, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QrImageView( + data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + size: qrImageSize, + backgroundColor: + Theme.of(context).extension()!.background, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + DetailItem( + title: "Encoded transaction config", + detail: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostTxData.state).state!.frostMSConfig!, + ), + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + DetailItem( + title: "Threshold", + detail: "$_threshold signatures", + horizontal: true, + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + if (!Util.isDesktop) + const Spacer( + flex: 2, + ), + CheckboxTextButton( + label: "I have verified that everyone has imported the config and " + "is ready to sign", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + SizedBox( + height: Util.isDesktop ? 20 : 12, + ), + PrimaryButton( + label: "Attempt sign", + enabled: _userVerifyContinue, + onPressed: () { + _attemptSign(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart new file mode 100644 index 000000000..5015110aa --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostSendStep1b extends ConsumerStatefulWidget { + const FrostSendStep1b({super.key}); + + static const String routeName = "/FrostSendStep1b"; + static const String title = "Sign FROST transaction"; + + @override + ConsumerState createState() => _FrostSendStep1bState(); +} + +class _FrostSendStep1bState extends ConsumerState { + static const info = [ + "Scan the config QR code or paste the code provided by the member " + "initiating this transaction.", + "Wait for other members to finish entering their information.", + "Verify that everyone has filled out their forms before continuing. If you " + "try to continue before everyone is ready, the process will be " + "canceled.", + "Check the box and press “Start signing”.", + ]; + + late final TextEditingController configFieldController; + late final FocusNode configFocusNode; + + bool _configEmpty = true, _userVerifyContinue = false; + + bool _attemptSignLock = false; + + Future _attemptSign() async { + if (_attemptSignLock) { + return; + } + + _attemptSignLock = true; + + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + + final config = configFieldController.text; + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + final data = Frost.extractDataFromSignConfig( + signConfig: config, + coin: wallet.cryptoCurrency, + ); + + final utxos = await ref + .read(mainDBProvider) + .getUTXOs(wallet.walletId) + .filter() + .anyOf( + data.inputs, + (q, e) => q + .txidEqualTo(Format.uint8listToString(e.hash)) + .and() + .valueEqualTo(e.value) + .and() + .voutEqualTo(e.vout)) + .findAll(); + + // TODO add more data from 'data' and display to user ? + ref.read(pFrostTxData.notifier).state = TxData( + frostMSConfig: config, + recipients: data.recipients + .map((e) => (address: e.address, amount: e.amount, isChange: false)) + .toList(), + utxos: utxos.toSet(), + ); + + final attemptSignRes = await wallet.frostAttemptSignConfig( + config: ref.read(pFrostTxData.state).state!.frostMSConfig!, + ); + + ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes; + + ref.read(pFrostCreateCurrentStep.state).state = 2; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Error, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Import and attempt sign config failed", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _attemptSignLock = false; + } + } + + @override + void initState() { + configFieldController = TextEditingController(); + configFocusNode = FocusNode(); + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(pFrostMyName.state).state = wallet.frostInfo.myName; + }); + super.initState(); + } + + @override + void dispose() { + configFieldController.dispose(); + configFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox(height: 20), + FrostStepField( + controller: configFieldController, + focusNode: configFocusNode, + showQrScanOption: true, + label: "Import sign config", + hint: "Enter config", + onChanged: (_) { + setState(() { + _configEmpty = configFieldController.text.isEmpty; + }); + }, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + CheckboxTextButton( + label: "I have verified that everyone has imported he config and" + " is ready to sign", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Start signing", + enabled: !_configEmpty && _userVerifyContinue, + onPressed: () { + _attemptSign(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart new file mode 100644 index 000000000..7fbea39da --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart @@ -0,0 +1,311 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostSendStep2 extends ConsumerStatefulWidget { + const FrostSendStep2({super.key}); + + static const String routeName = "/FrostSendStep2"; + static const String title = "Preprocesses"; + + @override + ConsumerState createState() => _FrostSendStep2State(); +} + +class _FrostSendStep2State extends ConsumerState { + final List controllers = []; + final List focusNodes = []; + + late final String myName; + late final List participantsWithoutMe; + late final String myPreprocess; + late final int myIndex; + late final int threshold; + + final List fieldIsEmptyFlags = []; + + int countPreprocesses() { + // own preprocess is not included in controllers and must be set here + int count = 1; + + for (final controller in controllers) { + if (controller.text.isNotEmpty) { + count++; + } + } + + return count; + } + + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + final frostInfo = wallet.frostInfo; + + myName = frostInfo.myName; + threshold = frostInfo.threshold; + participantsWithoutMe = + List.from(frostInfo.participants); // Copy so it isn't fixed-length. + myIndex = participantsWithoutMe.indexOf(frostInfo.myName); + myPreprocess = ref.read(pFrostAttemptSignData.state).state!.preprocess; + + participantsWithoutMe.removeAt(myIndex); + + for (int i = 0; i < participantsWithoutMe.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "1.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + "Share your preprocess with other signing group members.", + style: STextStyles.w500_12(context), + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "2.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: + "Enter their preprocesses into the corresponding fields. ", + style: STextStyles.w500_12(context), + ), + TextSpan( + text: "You must have the threshold number of " + "preprocesses (including yours) to send this transaction.", + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Threshold", + detail: "$threshold signatures", + horizontal: true, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My name", + detail: myName, + button: Util.isDesktop + ? IconCopyButton( + data: myName, + ) + : SimpleCopyButton( + data: myName, + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My preprocess", + detail: myPreprocess, + button: Util.isDesktop + ? IconCopyButton( + data: myPreprocess, + ) + : SimpleCopyButton( + data: myPreprocess, + ), + ), + const SizedBox(height: 12), + FrostQrDialogPopupButton( + data: myPreprocess, + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Text( + "You need to obtain ${threshold - 1} preprocess from signing members to send this transaction.", + style: STextStyles.label(context), + ), + ), + const SizedBox( + height: 12, + ), + Builder(builder: (context) { + final count = countPreprocesses(); + final colors = Theme.of(context).extension()!; + return DetailItem( + title: "Required preprocesses", + detail: "$count of $threshold", + horizontal: true, + overrideDetailTextColor: count >= threshold + ? colors.accentColorGreen + : colors.accentColorRed, + ); + }), + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < participantsWithoutMe.length; i++) + FrostStepField( + label: participantsWithoutMe[i], + hint: "Enter ${participantsWithoutMe[i]}'s preprocess", + controller: controllers[i], + focusNode: focusNodes[i], + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + showQrScanOption: true, + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Generate shares", + enabled: countPreprocesses() >= threshold, + onPressed: () async { + // collect Preprocess strings (not including my own) + final preprocesses = controllers.map((e) => e.text).toList(); + + // collect participants who are involved in this transaction + final List requiredParticipantsUnordered = []; + for (int i = 0; i < participantsWithoutMe.length; i++) { + if (preprocesses[i].isNotEmpty) { + requiredParticipantsUnordered.add(participantsWithoutMe[i]); + } + } + ref.read(pFrostSelectParticipantsUnordered.notifier).state = + requiredParticipantsUnordered; + + // insert an empty string at my index + preprocesses.insert(myIndex, ""); + + try { + ref.read(pFrostContinueSignData.notifier).state = + Frost.continueSigning( + machinePtr: + ref.read(pFrostAttemptSignData.state).state!.machinePtr, + preprocesses: preprocesses, + ); + + ref.read(pFrostCreateCurrentStep.state).state = 3; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + + // await Navigator.of(context).pushNamed( + // FrostContinueSignView.routeName, + // arguments: widget.walletId, + // ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => const FrostErrorDialog( + title: "Failed to continue signing", + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart new file mode 100644 index 000000000..46bb5cdb0 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart @@ -0,0 +1,253 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart'; +import 'package:stackwallet/widgets/frost_step_user_steps.dart'; +import 'package:stackwallet/widgets/textfields/frost_step_field.dart'; + +class FrostSendStep3 extends ConsumerStatefulWidget { + const FrostSendStep3({super.key}); + + static const String routeName = "/FrostSendStep3"; + static const String title = "Shares"; + + @override + ConsumerState createState() => _FrostSendStep3State(); +} + +class _FrostSendStep3State extends ConsumerState { + static const info = [ + "Send your share to other signing group members.", + "Enter their shares into the corresponding fields.", + ]; + + final List controllers = []; + final List focusNodes = []; + + late final String myName; + late final List participantsWithoutMe; + late final List participantsAll; + late final String myShare; + late final int myIndex; + + final List fieldIsEmptyFlags = []; + + bool _userVerifyContinue = false; + + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + final frostInfo = wallet.frostInfo; + + myName = frostInfo.myName; + participantsAll = frostInfo.participants; + myIndex = frostInfo.participants.indexOf(frostInfo.myName); + myShare = ref.read(pFrostContinueSignData.state).state!.share; + + participantsWithoutMe = frostInfo.participants + .toSet() + .intersection( + ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet()) + .toList(); + + participantsWithoutMe.remove(myName); + + for (int i = 0; i < participantsWithoutMe.length; i++) { + controllers.add(TextEditingController()); + focusNodes.add(FocusNode()); + fieldIsEmptyFlags.add(true); + } + super.initState(); + } + + @override + void dispose() { + for (int i = 0; i < controllers.length; i++) { + controllers[i].dispose(); + } + for (int i = 0; i < focusNodes.length; i++) { + focusNodes[i].dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const FrostStepUserSteps( + userSteps: info, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My name", + detail: myName, + button: Util.isDesktop + ? IconCopyButton( + data: myName, + ) + : SimpleCopyButton( + data: myName, + ), + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "My share", + detail: myShare, + button: Util.isDesktop + ? IconCopyButton( + data: myShare, + ) + : SimpleCopyButton( + data: myShare, + ), + ), + const SizedBox( + height: 12, + ), + FrostQrDialogPopupButton( + data: myShare, + ), + const SizedBox( + height: 12, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < participantsWithoutMe.length; i++) + FrostStepField( + label: participantsWithoutMe[i], + hint: "Enter ${participantsWithoutMe[i]}'s share", + controller: controllers[i], + focusNode: focusNodes[i], + onChanged: (_) { + setState(() { + fieldIsEmptyFlags[i] = controllers[i].text.isEmpty; + }); + }, + showQrScanOption: true, + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + CheckboxTextButton( + label: "I have verified that everyone has my share", + onChanged: (value) { + setState(() { + _userVerifyContinue = value; + }); + }, + ), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Generate transaction", + enabled: _userVerifyContinue && + !fieldIsEmptyFlags.reduce((v, e) => v |= e), + onPressed: () async { + // collect Share strings + final sharesCollected = controllers.map((e) => e.text).toList(); + + final List shares = []; + for (final participant in participantsAll) { + if (participantsWithoutMe.contains(participant)) { + shares.add(sharesCollected[ + participantsWithoutMe.indexOf(participant)]); + } else { + shares.add(""); + } + } + + try { + final rawTx = Frost.completeSigning( + machinePtr: + ref.read(pFrostContinueSignData.state).state!.machinePtr, + shares: shares, + ); + + final tx = cl.Transaction.fromHex(rawTx); + final txData = ref.read(pFrostTxData)!; + + final fractionDigits = + txData.recipients!.first.amount.fractionDigits; + + final inputTotal = Amount( + rawValue: txData.utxos! + .map((e) => BigInt.from(e.value)) + .reduce((v, e) => v += e), + fractionDigits: fractionDigits, + ); + final outputTotal = Amount( + rawValue: + tx.outputs.map((e) => e.value).reduce((v, e) => v += e), + fractionDigits: fractionDigits, + ); + + ref.read(pFrostTxData.state).state = txData.copyWith( + raw: rawTx, + fee: inputTotal - outputTotal, + frostSigners: [ + myName, + ...participantsWithoutMe, + ], + ); + + ref.read(pFrostCreateCurrentStep.state).state = 4; + await Navigator.of(context).pushNamed( + ref + .read(pFrostScaffoldArgs)! + .stepRoutes[ref.read(pFrostCreateCurrentStep) - 1] + .routeName, + ); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => const FrostErrorDialog( + title: "Failed to complete signing process", + ), + ); + } + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart new file mode 100644 index 000000000..2267e7226 --- /dev/null +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart @@ -0,0 +1,296 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/amount/amount_formatter.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/expandable.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FrostSendStep4 extends ConsumerStatefulWidget { + const FrostSendStep4({super.key}); + + static const String routeName = "/FrostSendStep4"; + static const String title = "Preview transaction"; + + @override + ConsumerState createState() => _FrostSendStep4State(); +} + +class _FrostSendStep4State extends ConsumerState { + final List _expandedStates = []; + + bool _broadcastLock = false; + + late final CryptoCurrency cryptoCurrency; + + @override + void initState() { + final wallet = ref.read(pWallets).getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) as BitcoinFrostWallet; + + cryptoCurrency = wallet.cryptoCurrency; + + for (final _ in ref.read(pFrostTxData)!.recipients!) { + _expandedStates.add(false); + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final signerNames = ref.watch(pFrostTxData)!.frostSigners!; + final recipients = ref.watch(pFrostTxData)!.recipients!; + + final String signers; + if (signerNames.length > 1) { + signers = signerNames + .sublist(1) + .fold(signerNames.first, (pv, e) => pv += ", $e"); + } else { + signers = signerNames.first; + } + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (kDebugMode) + DetailItem( + title: "Tx hex (debug mode only)", + detail: ref.watch(pFrostTxData)!.raw!, + button: Util.isDesktop + ? IconCopyButton( + data: ref.watch(pFrostTxData)!.raw!, + ) + : SimpleCopyButton( + data: ref.watch(pFrostTxData)!.raw!, + ), + ), + if (kDebugMode) + const SizedBox( + height: 12, + ), + Text( + "Send ${cryptoCurrency.coin.ticker}", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + recipients.length == 1 + ? _Recipient( + address: recipients[0].address, + amount: ref + .watch(pAmountFormatter(cryptoCurrency.coin)) + .format(recipients[0].amount), + ) + : Column( + children: [ + for (int i = 0; i < recipients.length; i++) + Padding( + padding: const EdgeInsets.only(top: 10), + child: Expandable( + onExpandChanged: (state) { + setState(() { + _expandedStates[i] = + state == ExpandableState.expanded; + }); + }, + header: Padding( + padding: const EdgeInsets.only(top: 12, bottom: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recipient ${i + 1}", + style: STextStyles.itemSubtitle(context), + ), + SvgPicture.asset( + _expandedStates[i] + ? Assets.svg.chevronUp + : Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ], + ), + ), + body: _Recipient( + address: recipients[i].address, + amount: ref + .watch(pAmountFormatter(cryptoCurrency.coin)) + .format(recipients[i].amount), + ), + ), + ), + ], + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Transaction fee", + detail: ref + .watch(pAmountFormatter(cryptoCurrency.coin)) + .format(ref.watch(pFrostTxData)!.fee!), + horizontal: true, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Total", + detail: ref.watch(pAmountFormatter(cryptoCurrency.coin)).format( + ref.watch(pFrostTxData)!.fee! + + recipients.map((e) => e.amount).reduce((v, e) => v += e)), + horizontal: true, + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Note", + detail: ref.watch(pFrostTxData)!.note ?? "", + ), + const SizedBox( + height: 12, + ), + DetailItem( + title: "Signers", + detail: signers, + ), + const SizedBox( + height: 12, + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Approve transaction", + onPressed: () async { + if (_broadcastLock) { + return; + } + _broadcastLock = true; + + try { + Exception? ex; + final txData = await showLoading( + whileFuture: ref + .read(pWallets) + .getWallet( + ref.read(pFrostScaffoldArgs)!.walletId!, + ) + .confirmSend( + txData: ref.read(pFrostTxData)!, + ), + context: context, + message: "Broadcasting transaction to network", + rootNavigator: true, // used to pop using root nav + onException: (e) { + ex = e; + }, + ); + + if (ex != null) { + throw ex!; + } + + if (context.mounted) { + if (txData != null) { + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + ref.read(pFrostTxData.state).state = txData; + ref.read(pFrostScaffoldArgs)!.parentNav.popUntil( + ModalRoute.withName( + Util.isDesktop + ? MyStackView.routeName + : WalletView.routeName, + ), + ); + } + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (context.mounted) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Broadcast error", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + onOkPressed: + Navigator.of(context, rootNavigator: true).pop, + ), + ); + } + } finally { + _broadcastLock = false; + } + }, + ), + ], + ), + ); + } +} + +class _Recipient extends StatelessWidget { + const _Recipient({ + super.key, + required this.address, + required this.amount, + }); + + final String address; + final String amount; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + DetailItem( + title: "Address", + detail: address, + ), + const SizedBox( + height: 6, + ), + DetailItem( + title: "Amount", + detail: amount, + horizontal: true, + ), + ], + ); + } +} diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 0ffeaafdc..fc0b89dc3 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -154,8 +154,10 @@ class _SendViewState extends ConsumerState { // .state = true, // ); - Logging.instance.log("qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); final results = AddressUtils.parseUri(qrResult.rawContent); @@ -213,8 +215,9 @@ class _SendViewState extends ConsumerState { // here we ignore the exception caused by not giving permission // to use the camera to scan a qr code Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } } @@ -280,8 +283,10 @@ class _SendViewState extends ConsumerState { return; } _cachedAmountToSend = amount; - Logging.instance.log("it changed $amount $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $amount $_cachedAmountToSend", + level: LogLevel.Info, + ); final price = ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; @@ -572,9 +577,10 @@ class _SendViewState extends ConsumerState { child: Text( "Cancel", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), onPressed: () { Navigator.of(context).pop(false); @@ -616,6 +622,9 @@ class _SendViewState extends ConsumerState { builder: (context) { return BuildingTransactionDialog( coin: wallet.info.coin, + isSpark: wallet is FiroWallet && + ref.read(publicPrivateBalanceStateProvider.state).state == + FiroType.spark, onCancel: () { wasCancelled = true; @@ -645,7 +654,7 @@ class _SendViewState extends ConsumerState { address: widget.accountLite!.code, amount: amount, isChange: false, - ) + ), ], satsPerVByte: isCustomFee ? customFeeRate : null, feeRateType: feeRate, @@ -668,7 +677,7 @@ class _SendViewState extends ConsumerState { amount: amount, memo: memoController.text, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -687,7 +696,7 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -709,7 +718,7 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], ), ); @@ -725,7 +734,7 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], sparkRecipients: ref.read(pValidSparkSendToAddress) ? [ @@ -734,7 +743,7 @@ class _SendViewState extends ConsumerState { amount: amount, memo: memoController.text, isChange: false, - ) + ), ] : null, ), @@ -752,7 +761,7 @@ class _SendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], memo: memo, feeRateType: ref.read(feeRateTypeStateProvider), @@ -827,9 +836,10 @@ class _SendViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), onPressed: () { Navigator.of(context).pop(); @@ -906,6 +916,10 @@ class _SendViewState extends ConsumerState { sendToController.text = _data!.contactLabel; _address = _data!.address.trim(); _addressToggleFlag = true; + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _setValidAddressProviders(_address); + }); } if (isPaynymSend) { @@ -977,7 +991,8 @@ class _SendViewState extends ConsumerState { debugPrint("BUILD: $runtimeType"); final wallet = ref.watch(pWallets).getWallet(walletId); final String locale = ref.watch( - localeServiceChangeNotifierProvider.select((value) => value.locale)); + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); final showCoinControl = wallet is CoinControlInterface && ref.watch( @@ -1029,7 +1044,7 @@ class _SendViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 50)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -1114,82 +1129,93 @@ class _SendViewState extends ConsumerState { ], ), const Spacer(), - Builder(builder: (context) { - final Amount amount; - if (isFiro) { - switch (ref - .watch( + Builder( + builder: (context) { + final Amount amount; + if (isFiro) { + switch (ref + .watch( publicPrivateBalanceStateProvider - .state) - .state) { - case FiroType.public: - amount = ref - .read(pWalletBalance(walletId)) - .spendable; - break; - case FiroType.lelantus: - amount = ref - .read(pWalletBalanceSecondary( - walletId)) - .spendable; - break; - case FiroType.spark: - amount = ref - .read(pWalletBalanceTertiary( - walletId)) - .spendable; - break; - } - } else { - amount = ref - .read(pWalletBalance(walletId)) - .spendable; - } - - return GestureDetector( - onTap: () { - cryptoAmountController.text = ref - .read(pAmountFormatter(coin)) - .format( - amount, - withUnitName: false, - ); - }, - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - Text( - ref - .watch(pAmountFormatter(coin)) - .format(amount), - style: STextStyles.titleBold12( - context) - .copyWith( - fontSize: 10, - ), - textAlign: TextAlign.right, - ), - Text( - "${(amount.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", - style: - STextStyles.subtitle(context) - .copyWith( - fontSize: 8, - ), - textAlign: TextAlign.right, + .state, ) - ], + .state) { + case FiroType.public: + amount = ref + .read(pWalletBalance(walletId)) + .spendable; + break; + case FiroType.lelantus: + amount = ref + .read( + pWalletBalanceSecondary( + walletId, + ), + ) + .spendable; + break; + case FiroType.spark: + amount = ref + .read( + pWalletBalanceTertiary( + walletId, + ), + ) + .spendable; + break; + } + } else { + amount = ref + .read(pWalletBalance(walletId)) + .spendable; + } + + return GestureDetector( + onTap: () { + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format( + amount, + withUnitName: false, + ); + }, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + ref + .watch( + pAmountFormatter(coin), + ) + .format(amount), + style: STextStyles.titleBold12( + context, + ).copyWith( + fontSize: 10, + ), + textAlign: TextAlign.right, + ), + Text( + "${(amount.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount( + fractionDigits: 2, + ).fiatString( + locale: locale, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.subtitle( + context, + ).copyWith( + fontSize: 8, + ), + textAlign: TextAlign.right, + ), + ], + ), ), - ), - ); - }), + ); + }, + ), ], ), ), @@ -1291,12 +1317,14 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Clear Button. Clears The Address Field Input.", key: const Key( - "sendViewClearAddressFieldButtonKey"), + "sendViewClearAddressFieldButtonKey", + ), onTap: () { sendToController.text = ""; _address = ""; _setValidAddressProviders( - _address); + _address, + ); setState(() { _addressToggleFlag = false; @@ -1308,12 +1336,13 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Paste Button. Pastes From Clipboard To Address Field Input.", key: const Key( - "sendViewPasteAddressFieldButtonKey"), + "sendViewPasteAddressFieldButtonKey", + ), onTap: () async { final ClipboardData? data = await clipboard.getData( - Clipboard - .kTextPlain); + Clipboard.kTextPlain, + ); if (data?.text != null && data! .text!.isNotEmpty) { @@ -1323,23 +1352,27 @@ class _SendViewState extends ConsumerState { .contains("\n")) { content = content.substring( - 0, - content.indexOf( - "\n")); + 0, + content.indexOf( + "\n", + ), + ); } if (coin == Coin.epicCash) { // strip http:// and https:// if content contains @ content = formatAddress( - content); + content, + ); } sendToController.text = content.trim(); _address = content.trim(); _setValidAddressProviders( - _address); + _address, + ); setState(() { _addressToggleFlag = sendToController @@ -1358,7 +1391,8 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Address Book Button. Opens Address Book For Address Field.", key: const Key( - "sendViewAddressBookButtonKey"), + "sendViewAddressBookButtonKey", + ), onTap: () { Navigator.of(context).pushNamed( AddressBookView.routeName, @@ -1372,7 +1406,8 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Scan QR Button. Opens Camera For Scanning QR Code.", key: const Key( - "sendViewScanQrButtonKey"), + "sendViewScanQrButtonKey", + ), onTap: _scanQr, child: const QrCodeIcon(), ), @@ -1389,7 +1424,8 @@ class _SendViewState extends ConsumerState { if (isStellar || (ref.watch(pValidSparkSendToAddress) && ref.watch( - publicPrivateBalanceStateProvider) != + publicPrivateBalanceStateProvider, + ) != FiroType.lelantus)) ClipRRect( borderRadius: BorderRadius.circular( @@ -1436,7 +1472,8 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Clear Button. Clears The Memo Field Input.", key: const Key( - "sendViewClearMemoFieldButtonKey"), + "sendViewClearMemoFieldButtonKey", + ), onTap: () { memoController.text = ""; setState(() {}); @@ -1447,16 +1484,17 @@ class _SendViewState extends ConsumerState { semanticsLabel: "Paste Button. Pastes From Clipboard To Memo Field Input.", key: const Key( - "sendViewPasteMemoFieldButtonKey"), + "sendViewPasteMemoFieldButtonKey", + ), onTap: () async { final ClipboardData? data = await clipboard.getData( - Clipboard - .kTextPlain); + Clipboard.kTextPlain, + ); if (data?.text != null && data! .text!.isNotEmpty) { - String content = + final String content = data.text!.trim(); memoController.text = @@ -1482,13 +1520,15 @@ class _SendViewState extends ConsumerState { error = null; } else if (isFiro) { if (ref.watch( - publicPrivateBalanceStateProvider) == + publicPrivateBalanceStateProvider, + ) == FiroType.lelantus) { if (_data != null && _data!.contactLabel == _address) { error = SparkInterface.validateSparkAddress( - address: _data!.address, - isTestNet: coin.isTestNet) + address: _data!.address, + isTestNet: coin.isTestNet, + ) ? "Unsupported" : null; } else if (ref @@ -1607,51 +1647,65 @@ class _SendViewState extends ConsumerState { Text( "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", style: STextStyles.itemSubtitle12( - context), + context, + ), ), const SizedBox( width: 10, ), - Builder(builder: (_) { - final Amount amount; - switch (ref - .read( + Builder( + builder: (_) { + final Amount amount; + switch (ref + .read( publicPrivateBalanceStateProvider - .state) - .state) { - case FiroType.public: - amount = ref - .watch(pWalletBalance( - walletId)) - .spendable; - break; - case FiroType.lelantus: - amount = ref - .watch( + .state, + ) + .state) { + case FiroType.public: + amount = ref + .watch( + pWalletBalance( + walletId, + ), + ) + .spendable; + break; + case FiroType.lelantus: + amount = ref + .watch( pWalletBalanceSecondary( - walletId)) - .spendable; - break; - case FiroType.spark: - amount = ref - .watch( + walletId, + ), + ) + .spendable; + break; + case FiroType.spark: + amount = ref + .watch( pWalletBalanceTertiary( - walletId)) - .spendable; - break; - } + walletId, + ), + ) + .spendable; + break; + } - return Text( - ref - .watch( - pAmountFormatter(coin)) - .format( - amount, - ), - style: STextStyles.itemSubtitle( - context), - ); - }), + return Text( + ref + .watch( + pAmountFormatter(coin), + ) + .format( + amount, + ), + style: + STextStyles.itemSubtitle( + context, + ), + ); + }, + ), ], ), SvgPicture.asset( @@ -1665,7 +1719,7 @@ class _SendViewState extends ConsumerState { ], ), ), - ) + ), ], ), const SizedBox( @@ -1687,8 +1741,9 @@ class _SendViewState extends ConsumerState { final Amount amount; switch (ref .read( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state) { case FiroType.public: amount = ref @@ -1697,14 +1752,20 @@ class _SendViewState extends ConsumerState { break; case FiroType.lelantus: amount = ref - .read(pWalletBalanceSecondary( - walletId)) + .read( + pWalletBalanceSecondary( + walletId, + ), + ) .spendable; break; case FiroType.spark: amount = ref - .read(pWalletBalanceTertiary( - walletId)) + .read( + pWalletBalanceTertiary( + walletId, + ), + ) .spendable; break; } @@ -1789,9 +1850,10 @@ class _SendViewState extends ConsumerState { .unitForCoin(coin), style: STextStyles.smallMed14(context) .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1851,13 +1913,16 @@ class _SendViewState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(12), child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), + ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + ), style: STextStyles.smallMed14(context) .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1894,7 +1959,7 @@ class _SendViewState extends ConsumerState { ); } - if (mounted) { + if (context.mounted) { final spendable = ref .read(pWalletBalance(walletId)) .spendable; @@ -2092,8 +2157,9 @@ class _SendViewState extends ConsumerState { onPressed: isFiro && ref .watch( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state != FiroType.public ? null @@ -2113,8 +2179,9 @@ class _SendViewState extends ConsumerState { TransactionFeeSelectionSheet( walletId: walletId, amount: (Decimal.tryParse( - cryptoAmountController - .text) ?? + cryptoAmountController + .text, + ) ?? ref .watch(pSendAmount) ?.decimal ?? @@ -2150,8 +2217,9 @@ class _SendViewState extends ConsumerState { child: (isFiro && ref .watch( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state != FiroType.public) ? Row( @@ -2171,7 +2239,8 @@ class _SendViewState extends ConsumerState { "~${snapshot.data!}", style: STextStyles .itemSubtitle( - context), + context, + ), ); } else { return AnimatedText( @@ -2183,7 +2252,8 @@ class _SendViewState extends ConsumerState { ], style: STextStyles .itemSubtitle( - context), + context, + ), ); } }, @@ -2199,13 +2269,15 @@ class _SendViewState extends ConsumerState { Text( ref .watch( - feeRateTypeStateProvider - .state) + feeRateTypeStateProvider + .state, + ) .state .prettyName, style: STextStyles .itemSubtitle12( - context), + context, + ), ), const SizedBox( width: 10, @@ -2229,7 +2301,8 @@ class _SendViewState extends ConsumerState { : "~${snapshot.data!}", style: STextStyles .itemSubtitle( - context), + context, + ), ); } else { return AnimatedText( @@ -2241,7 +2314,8 @@ class _SendViewState extends ConsumerState { ], style: STextStyles .itemSubtitle( - context), + context, + ), ); } }, @@ -2259,7 +2333,7 @@ class _SendViewState extends ConsumerState { ], ), ), - ) + ), ], ), if (isCustomFee) diff --git a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart index a8dfe643b..8529925d6 100644 --- a/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart +++ b/lib/pages/send_view/sub_widgets/building_transaction_dialog.dart @@ -12,8 +12,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/coin_image_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -23,13 +23,15 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; class BuildingTransactionDialog extends ConsumerStatefulWidget { const BuildingTransactionDialog({ - Key? key, + super.key, required this.onCancel, required this.coin, - }) : super(key: key); + required this.isSpark, + }); final VoidCallback onCancel; final Coin coin; + final bool isSpark; @override ConsumerState createState() => @@ -62,13 +64,24 @@ class _RestoringDialogState extends ConsumerState { "Generating transaction", style: STextStyles.desktopH3(context), ), + if (widget.isSpark) + const SizedBox( + height: 16, + ), + if (widget.isSpark) + Text( + "This may take a few minutes...", + style: STextStyles.desktopSubtitleH2(context), + ), const SizedBox( height: 40, ), assetPath.endsWith(".gif") - ? Image.file(File( - assetPath, - )) + ? Image.file( + File( + assetPath, + ), + ) : const RotatingArrows( width: 40, height: 40, @@ -82,7 +95,7 @@ class _RestoringDialogState extends ConsumerState { onPressed: () { onCancel.call(); }, - ) + ), ], ); } else { @@ -96,14 +109,26 @@ class _RestoringDialogState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - Image.file(File( - assetPath, - )), + Image.file( + File( + assetPath, + ), + ), Text( "Generating transaction", textAlign: TextAlign.center, style: STextStyles.pageTitleH2(context), ), + if (widget.isSpark) + const SizedBox( + height: 12, + ), + if (widget.isSpark) + Text( + "This may take a few minutes...", + textAlign: TextAlign.center, + style: STextStyles.w500_16(context), + ), const SizedBox( height: 32, ), @@ -124,7 +149,7 @@ class _RestoringDialogState extends ConsumerState { onCancel.call(); }, ), - ) + ), ], ), ], @@ -132,6 +157,8 @@ class _RestoringDialogState extends ConsumerState { ) : StackDialog( title: "Generating transaction", + message: + widget.isSpark ? "This may take a few minutes..." : null, icon: const RotatingArrows( width: 24, height: 24, diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart index 8c01cac9a..6206f13f7 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart @@ -173,85 +173,91 @@ class _FiroBalanceSelectionSheetState ), ), ), - const SizedBox( - height: 16, - ), - GestureDetector( - onTap: () { - final state = - ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != FiroType.lelantus) { - ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.lelantus; - } - Navigator.of(context).pop(); - }, - child: Container( - color: Colors.transparent, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: FiroType.lelantus, - groupValue: ref - .watch( - publicPrivateBalanceStateProvider.state) - .state, - onChanged: (x) { - ref - .read(publicPrivateBalanceStateProvider - .state) - .state = FiroType.lelantus; - - Navigator.of(context).pop(); - }, - ), - ), - ], - ), - const SizedBox( - width: 12, - ), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (firoWallet.info.cachedBalanceSecondary.spendable.raw > + BigInt.zero) + const SizedBox( + height: 16, + ), + if (firoWallet.info.cachedBalanceSecondary.spendable.raw > + BigInt.zero) + GestureDetector( + onTap: () { + final state = ref + .read(publicPrivateBalanceStateProvider.state) + .state; + if (state != FiroType.lelantus) { + ref + .read(publicPrivateBalanceStateProvider.state) + .state = FiroType.lelantus; + } + Navigator.of(context).pop(); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, children: [ - // Row( - // children: [ - Text( - "Lelantus balance", - style: STextStyles.titleBold12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - width: 2, - ), - Text( - ref.watch(pAmountFormatter(coin)).format( - firoWallet.info.cachedBalanceSecondary - .spendable, - ), - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, + SizedBox( + width: 20, + height: 20, + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: FiroType.lelantus, + groupValue: ref + .watch(publicPrivateBalanceStateProvider + .state) + .state, + onChanged: (x) { + ref + .read(publicPrivateBalanceStateProvider + .state) + .state = FiroType.lelantus; + + Navigator.of(context).pop(); + }, + ), ), ], ), - // ], - // ), - ) - ], + const SizedBox( + width: 12, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Row( + // children: [ + Text( + "Lelantus balance", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + width: 2, + ), + Text( + ref.watch(pAmountFormatter(coin)).format( + firoWallet.info.cachedBalanceSecondary + .spendable, + ), + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + // ], + // ), + ) + ], + ), ), ), - ), const SizedBox( height: 16, ), diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index f2178a450..8572d5037 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'package:stackwallet/widgets/animated_text.dart'; final feeSheetSessionCacheProvider = @@ -697,7 +698,7 @@ class _TransactionFeeSelectionSheetState const SizedBox( height: 24, ), - if (coin.isElectrumXCoin) + if (wallet is ElectrumXInterface) GestureDetector( onTap: () { final state = @@ -766,7 +767,7 @@ class _TransactionFeeSelectionSheetState ), ), ), - if (coin.isElectrumXCoin) + if (wallet is ElectrumXInterface) const SizedBox( height: 24, ), diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 0cba2bf17..028538d63 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -58,14 +58,14 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class TokenSendView extends ConsumerStatefulWidget { const TokenSendView({ - Key? key, + super.key, required this.walletId, required this.coin, required this.tokenContract, this.autoFillData, this.clipboard = const ClipboardWrapper(), this.barcodeScanner = const BarcodeScannerWrapper(), - }) : super(key: key); + }); static const String routeName = "/tokenSendView"; @@ -156,8 +156,10 @@ class _TokenSendViewState extends ConsumerState { // .state = true, // ); - Logging.instance.log("qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); final results = AddressUtils.parseUri(qrResult.rawContent); @@ -216,8 +218,9 @@ class _TokenSendViewState extends ConsumerState { // here we ignore the exception caused by not giving permission // to use the camera to scan a qr code Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } } @@ -239,15 +242,19 @@ class _TokenSendViewState extends ConsumerState { ? Amount.zero : Amount.fromDecimal( (baseAmount.decimal / _price).toDecimal( - scaleOnInfinitePrecision: tokenContract.decimals), - fractionDigits: tokenContract.decimals); + scaleOnInfinitePrecision: tokenContract.decimals, + ), + fractionDigits: tokenContract.decimals, + ); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; } _cachedAmountToSend = _amountToSend; - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info, + ); _cryptoAmountChangeLock = true; cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( @@ -282,8 +289,10 @@ class _TokenSendViewState extends ConsumerState { return; } _cachedAmountToSend = _amountToSend; - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info, + ); final price = ref .read(priceAnd24hChangeNotifierProvider) @@ -457,6 +466,7 @@ class _TokenSendViewState extends ConsumerState { builder: (context) { return BuildingTransactionDialog( coin: wallet.info.coin, + isSpark: false, onCancel: () { wasCancelled = true; @@ -484,7 +494,7 @@ class _TokenSendViewState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), note: noteController.text, @@ -502,20 +512,22 @@ class _TokenSendViewState extends ConsumerState { // pop building dialog Navigator.of(context).pop(); - unawaited(Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: txData, - walletId: walletId, - isTokenTx: true, - onSuccess: clearSendForm, - ), - settings: const RouteSettings( - name: ConfirmTransactionView.routeName, + unawaited( + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => ConfirmTransactionView( + txData: txData, + walletId: walletId, + isTokenTx: true, + onSuccess: clearSendForm, + ), + settings: const RouteSettings( + name: ConfirmTransactionView.routeName, + ), ), ), - )); + ); } } catch (e) { if (mounted) { @@ -538,9 +550,10 @@ class _TokenSendViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), onPressed: () { Navigator.of(context).pop(); @@ -626,7 +639,8 @@ class _TokenSendViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); final String locale = ref.watch( - localeServiceChangeNotifierProvider.select((value) => value.locale)); + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); return Background( child: Scaffold( @@ -638,7 +652,7 @@ class _TokenSendViewState extends ConsumerState { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 50)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -712,11 +726,15 @@ class _TokenSendViewState extends ConsumerState { .watch(pAmountFormatter(coin)) .format( ref - .read(pTokenBalance(( - walletId: widget.walletId, - contractAddress: - tokenContract.address, - ))) + .read( + pTokenBalance( + ( + walletId: widget.walletId, + contractAddress: + tokenContract.address, + ), + ), + ) .spendable, ethContract: tokenContract, withUnitName: false, @@ -734,13 +752,17 @@ class _TokenSendViewState extends ConsumerState { .watch(pAmountFormatter(coin)) .format( ref - .watch(pTokenBalance(( - walletId: - widget.walletId, - contractAddress: - tokenContract - .address, - ))) + .watch( + pTokenBalance( + ( + walletId: + widget.walletId, + contractAddress: + tokenContract + .address, + ), + ), + ) .spendable, ethContract: tokenContract, ), @@ -752,13 +774,17 @@ class _TokenSendViewState extends ConsumerState { textAlign: TextAlign.right, ), Text( - "${(ref.watch(pTokenBalance(( + "${(ref.watch( + pTokenBalance( + ( walletId: widget.walletId, contractAddress: tokenContract .address, - ))).spendable.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( + ), + ), + ).spendable.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( fractionDigits: 2, ).fiatString( locale: locale, @@ -768,7 +794,7 @@ class _TokenSendViewState extends ConsumerState { fontSize: 8, ), textAlign: TextAlign.right, - ) + ), ], ), ), @@ -807,7 +833,9 @@ class _TokenSendViewState extends ConsumerState { onChanged: (newValue) { _address = newValue.trim(); _updatePreviewButtonState( - _address, _amountToSend); + _address, + _amountToSend, + ); setState(() { _addressToggleFlag = newValue.isNotEmpty; @@ -838,12 +866,15 @@ class _TokenSendViewState extends ConsumerState { _addressToggleFlag ? TextFieldIconButton( key: const Key( - "tokenSendViewClearAddressFieldButtonKey"), + "tokenSendViewClearAddressFieldButtonKey", + ), onTap: () { sendToController.text = ""; _address = ""; _updatePreviewButtonState( - _address, _amountToSend); + _address, + _amountToSend, + ); setState(() { _addressToggleFlag = false; }); @@ -852,7 +883,8 @@ class _TokenSendViewState extends ConsumerState { ) : TextFieldIconButton( key: const Key( - "tokenSendViewPasteAddressFieldButtonKey"), + "tokenSendViewPasteAddressFieldButtonKey", + ), onTap: _onTokenSendViewPasteAddressFieldButtonPressed, child: sendToController @@ -863,7 +895,8 @@ class _TokenSendViewState extends ConsumerState { if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key( - "sendViewAddressBookButtonKey"), + "sendViewAddressBookButtonKey", + ), onTap: () { Navigator.of(context).pushNamed( AddressBookView.routeName, @@ -875,11 +908,12 @@ class _TokenSendViewState extends ConsumerState { if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key( - "sendViewScanQrButtonKey"), + "sendViewScanQrButtonKey", + ), onTap: _onTokenSendViewScanQrButtonPressed, child: const QrCodeIcon(), - ) + ), ], ), ), @@ -997,9 +1031,10 @@ class _TokenSendViewState extends ConsumerState { tokenContract.symbol, style: STextStyles.smallMed14(context) .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1058,13 +1093,16 @@ class _TokenSendViewState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(12), child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), + ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + ), style: STextStyles.smallMed14(context) .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1169,8 +1207,8 @@ class _TokenSendViewState extends ConsumerState { walletId: walletId, isToken: true, amount: (Decimal.tryParse( - cryptoAmountController - .text) ?? + cryptoAmountController.text, + ) ?? Decimal.zero) .toAmount( fractionDigits: @@ -1193,12 +1231,15 @@ class _TokenSendViewState extends ConsumerState { children: [ Text( ref - .watch(feeRateTypeStateProvider - .state) + .watch( + feeRateTypeStateProvider + .state, + ) .state .prettyName, style: STextStyles.itemSubtitle12( - context), + context, + ), ), const SizedBox( width: 10, @@ -1213,7 +1254,8 @@ class _TokenSendViewState extends ConsumerState { "~${snapshot.data!}", style: STextStyles.itemSubtitle( - context), + context, + ), ); } else { return AnimatedText( @@ -1225,7 +1267,8 @@ class _TokenSendViewState extends ConsumerState { ], style: STextStyles.itemSubtitle( - context), + context, + ), ); } }, @@ -1243,7 +1286,7 @@ class _TokenSendViewState extends ConsumerState { ], ), ), - ) + ), ], ), const Spacer(), @@ -1253,13 +1296,15 @@ class _TokenSendViewState extends ConsumerState { TextButton( onPressed: ref .watch( - previewTokenTxButtonStateProvider.state) + previewTokenTxButtonStateProvider.state, + ) .state ? _previewTransaction : null, style: ref .watch( - previewTokenTxButtonStateProvider.state) + previewTokenTxButtonStateProvider.state, + ) .state ? Theme.of(context) .extension()! diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 6cc47a0d4..6ebafad4a 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -13,23 +13,21 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; -import 'package:stackwallet/widgets/onetime_popups/tor_has_been_add_dialog.dart'; +import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class HiddenSettings extends StatelessWidget { - const HiddenSettings({Key? key}) : super(key: key); + const HiddenSettings({super.key}); static const String routeName = "/hiddenSettings"; @@ -39,27 +37,25 @@ class HiddenSettings extends StatelessWidget { child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( - leading: Util.isDesktop - ? Padding( - padding: const EdgeInsets.all(8.0), - child: AppBarIconButton( - size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.arrowLeft, - width: 18, - height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: Navigator.of(context).pop, - ), - ) - : Container(), + leading: Padding( + padding: const EdgeInsets.all(8.0), + child: AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + ), title: Text( "Dev options", style: STextStyles.navBarTitle(context), @@ -176,49 +172,48 @@ class HiddenSettings extends StatelessWidget { const SizedBox( height: 12, ), - Consumer(builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - await showOneTimeTorHasBeenAddedDialogIfRequired( - context, - ); - }, - child: RoundedWhiteContainer( - child: Text( - "Test tor stacy popup", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }), - const SizedBox( - height: 12, - ), - Consumer(builder: (_, ref, __) { - return GestureDetector( - onTap: () async { - final box = await Hive.openBox( - DB.boxNameOneTimeDialogsShown); - await box.clear(); - }, - child: RoundedWhiteContainer( - child: Text( - "Reset tor stacy popup", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - ), - ); - }), - - const SizedBox( - height: 12, - ), + // Consumer(builder: (_, ref, __) { + // return GestureDetector( + // onTap: () async { + // await showOneTimeTorHasBeenAddedDialogIfRequired( + // context, + // ); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Test tor stacy popup", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .accentColorDark), + // ), + // ), + // ); + // }), + // const SizedBox( + // height: 12, + // ), + // Consumer(builder: (_, ref, __) { + // return GestureDetector( + // onTap: () async { + // final box = await Hive.openBox( + // DB.boxNameOneTimeDialogsShown); + // await box.clear(); + // }, + // child: RoundedWhiteContainer( + // child: Text( + // "Reset tor stacy popup", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .accentColorDark), + // ), + // ), + // ); + // }), + // const SizedBox( + // height: 12, + // ), Consumer( builder: (_, ref, __) { if (ref.watch(prefsChangeNotifierProvider @@ -252,6 +247,35 @@ class HiddenSettings extends StatelessWidget { } }, ), + const SizedBox( + height: 12, + ), + Consumer( + builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + await showDialog( + context: context, + builder: (_) => TorWarningDialog( + coin: Coin.stellar, + ), + ); + }, + child: RoundedWhiteContainer( + child: Text( + "Show Tor warning popup", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }, + ), + const SizedBox( + height: 12, + ), Consumer( builder: (_, ref, __) { return GestureDetector( diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index ddac06aca..7a656c288 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -14,18 +14,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:solana/solana.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart'; +import 'package:stackwallet/utilities/test_eth_node_connection.dart'; import 'package:stackwallet/utilities/test_monero_node_connection.dart'; import 'package:stackwallet/utilities/test_stellar_node_connection.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -166,22 +169,23 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: case Coin.dogecoinTestNet: - final client = ElectrumXClient( - host: formData.host!, - port: formData.port!, - useSSL: formData.useSSL!, - failovers: [], - prefs: ref.read(prefsChangeNotifierProvider), - coin: coin, - ); - try { - testPassed = await client.ping(); + testPassed = await checkElectrumServer( + host: formData.host!, + port: formData.port!, + useSSL: formData.useSSL!, + overridePrefs: ref.read(prefsChangeNotifierProvider), + overrideTorService: ref.read(pTorService), + ); } catch (_) { testPassed = false; } @@ -189,14 +193,13 @@ class _AddEditNodeViewState extends ConsumerState { break; case Coin.ethereum: - // TODO fix this - // final client = Web3Client( - // "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", - // Client()); try { - // await client.getSyncStatus(); - } catch (_) {} + testPassed = await testEthNodeConnection(formData.host!); + } catch (_) { + testPassed = false; + } break; + case Coin.stellar: case Coin.stellarTestnet: try { @@ -216,6 +219,21 @@ class _AddEditNodeViewState extends ConsumerState { ); } catch (_) {} break; + + case Coin.solana: + try { + RpcClient rpcClient; + if (formData.host!.startsWith("http") || + formData.host!.startsWith("https")) { + rpcClient = RpcClient("${formData.host}:${formData.port}"); + } else { + rpcClient = RpcClient("http://${formData.host}:${formData.port}"); + } + await rpcClient.getEpochInfo().then((value) => testPassed = true); + } catch (_) { + testPassed = false; + } + break; } if (showFlushBar && mounted) { @@ -746,6 +764,8 @@ class _NodeFormState extends ConsumerState { case Coin.namecoin: case Coin.bitcoincash: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.tezos: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: @@ -756,8 +776,11 @@ class _NodeFormState extends ConsumerState { case Coin.nano: case Coin.banano: case Coin.eCash: + case Coin.solana: case Coin.stellar: case Coin.stellarTestnet: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: return false; case Coin.ethereum: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index bd974b6ec..1034d986c 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -13,13 +13,15 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:solana/solana.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -140,6 +142,8 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.dogecoin: case Coin.firo: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: @@ -148,17 +152,16 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.eCash: - final client = ElectrumXClient( - host: node!.host, - port: node.port, - useSSL: node.useSSL, - failovers: [], - prefs: ref.read(prefsChangeNotifierProvider), - coin: coin, - ); - + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: try { - testPassed = await client.ping(); + testPassed = await checkElectrumServer( + host: node!.host, + port: node.port, + useSSL: node.useSSL, + overridePrefs: ref.read(prefsChangeNotifierProvider), + overrideTorService: ref.read(pTorService), + ); } catch (_) { testPassed = false; } @@ -193,6 +196,20 @@ class _NodeDetailsViewState extends ConsumerState { testPassed = false; } break; + + case Coin.solana: + try { + RpcClient rpcClient; + if (node!.host.startsWith("http") || node.host.startsWith("https")) { + rpcClient = RpcClient("${node.host}:${node.port}"); + } else { + rpcClient = RpcClient("http://${node.host}:${node.port}"); + } + await rpcClient.getEpochInfo().then((value) => testPassed = true); + } catch (_) { + testPassed = false; + } + break; } if (testPassed) { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index 1e0c01609..546c780e9 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -88,7 +88,7 @@ class _EnableAutoBackupViewState extends ConsumerState { passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); - if (Platform.isAndroid) { + if (Platform.isAndroid || Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -151,11 +151,11 @@ class _EnableAutoBackupViewState extends ConsumerState { const SizedBox( height: 10, ), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid + onTap: Platform.isAndroid || Platform.isIOS ? null : () async { try { @@ -213,7 +213,7 @@ class _EnableAutoBackupViewState extends ConsumerState { ), onChanged: (newValue) {}, ), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) const SizedBox( height: 10, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index 2c2af8ca7..4dfa049d1 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -80,7 +80,7 @@ class _RestoreFromFileViewState extends State { passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); - if (Platform.isAndroid) { + if (Platform.isAndroid || Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -179,14 +179,14 @@ class _RestoreFromFileViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) Consumer(builder: (context, ref, __) { return Container( color: Colors.transparent, child: TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid + onTap: Platform.isAndroid || Platform.isIOS ? null : () async { try { @@ -248,7 +248,7 @@ class _RestoreFromFileViewState extends State { ), ); }), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) SizedBox( height: !isDesktop ? 8 : 24, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 0838a25b3..f399c8266 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -260,7 +260,7 @@ class _EditAutoBackupViewState extends ConsumerState { passwordFocusNode = FocusNode(); passwordRepeatFocusNode = FocusNode(); - if (Platform.isAndroid) { + if (Platform.isAndroid || Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -346,11 +346,11 @@ class _EditAutoBackupViewState extends ConsumerState { const SizedBox( height: 10, ), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, - onTap: Platform.isAndroid + onTap: Platform.isAndroid || Platform.isIOS ? null : () async { try { @@ -418,7 +418,7 @@ class _EditAutoBackupViewState extends ConsumerState { ), textAlign: TextAlign.left, ), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) const SizedBox( height: 10, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 9891148d7..f6491cf1d 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -13,6 +13,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:frostdart/frostdart.dart' as frost; import 'package:isar/isar.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stackwallet/db/hive/db.dart'; @@ -26,6 +27,7 @@ import 'package:stackwallet/models/stack_restoring_ui_state.dart'; import 'package:stackwallet/models/trade_wallet_lookup.dart'; import 'package:stackwallet/models/wallet_restore_state.dart'; import 'package:stackwallet/services/address_book_service.dart'; +import 'package:stackwallet/services/frost.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/trade_notes_service.dart'; import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; @@ -41,7 +43,9 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; @@ -302,6 +306,24 @@ abstract class SWB { await wallet.getMnemonicPassphrase(); } else if (wallet is PrivateKeyInterface) { backupWallet['privateKey'] = await wallet.getPrivateKey(); + } else if (wallet is BitcoinFrostWallet) { + String? keys = await wallet.getSerializedKeys(); + String? config = await wallet.getMultisigConfig(); + if (keys == null || config == null) { + String err = "${wallet.info.coin.name} wallet ${wallet.info.name} " + "has null keys or config"; + Logging.instance.log(err, level: LogLevel.Fatal); + throw Exception(err); + } + //This case should never actually happen in practice unless the whole + // wallet is somehow corrupt + // TODO [prio=low]: solve case in which either keys or config is null. + + // Format keys & config as a JSON string and set otherDataJsonString. + Map frostData = {}; + frostData["keys"] = keys; + frostData["config"] = config; + backupWallet['frostWalletData'] = jsonEncode(frostData); } backupWallet['coinName'] = wallet.info.coin.name; backupWallet['storedChainHeight'] = wallet.info.cachedChainHeight; @@ -384,7 +406,9 @@ abstract class SWB { if (walletbackup['mnemonic'] == null) { // probably private key based - privateKey = walletbackup['privateKey'] as String; + if (walletbackup['privateKey'] != null) { + privateKey = walletbackup['privateKey'] as String; + } } else { if (walletbackup['mnemonic'] is List) { List mnemonicList = (walletbackup['mnemonic'] as List) @@ -406,6 +430,37 @@ abstract class SWB { ); try { + String? serializedKeys; + String? multisigConfig; + if (info.coin.isFrost) { + // Decode info.otherDataJsonString for Frost recovery info. + final frostData = jsonDecode(walletbackup["frostWalletData"] as String); + serializedKeys = frostData["keys"] as String; + multisigConfig = frostData["config"] as String; + + final myNameIndex = frost.getParticipantIndexFromKeys( + serializedKeys: serializedKeys, + ); + final participants = Frost.getParticipants( + multisigConfig: multisigConfig, + ); + final myName = participants[myNameIndex]; + + final frostInfo = FrostWalletInfo( + walletId: info.walletId, + knownSalts: [], + participants: participants, + myName: myName, + threshold: frost.multisigThreshold( + multisigConfig: multisigConfig, + ), + ); + + await MainDB.instance.isar.writeTxn(() async { + await MainDB.instance.isar.frostWalletInfo.put(frostInfo); + }); + } + final wallet = await Wallet.create( walletInfo: info, mainDB: MainDB.instance, @@ -427,7 +482,15 @@ abstract class SWB { Future? restoringFuture; if (!(wallet is CwBasedInterface || wallet is EpiccashWallet)) { - restoringFuture = wallet.recover(isRescan: false); + if (wallet is BitcoinFrostWallet) { + restoringFuture = wallet.recover( + isRescan: false, + multisigConfig: multisigConfig!, + serializedKeys: serializedKeys!, + ); + } else { + restoringFuture = wallet.recover(isRescan: false); + } } uiState?.update( diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart index 50611e634..046e8642f 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart @@ -79,11 +79,16 @@ class SWBFileSystem { } Future pickDir(BuildContext context) async { - final String? path = await FilePicker.platform.getDirectoryPath( - dialogTitle: "Choose Backup location", - initialDirectory: startPath!.path, - lockParentWindow: true, - ); + final String? path; + if (Platform.isIOS) { + path = startPath?.path; + } else { + path = await FilePicker.platform.getDirectoryPath( + dialogTitle: "Choose Backup location", + initialDirectory: startPath!.path, + lockParentWindow: true, + ); + } dirPath = path; } diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart index 4ff5da679..58144b681 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart @@ -11,7 +11,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart'; -import 'package:stackwallet/providers/global/active_wallet_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -95,12 +94,12 @@ class SyncingOptionsView extends ConsumerWidget { ref.read(prefsChangeNotifierProvider).syncType = SyncingType.currentWalletOnly; - // disable auto sync on all wallets that aren't active/current - ref.read(pWallets).wallets.forEach((e) { - if (e.walletId != ref.read(currentWalletIdProvider)) { - e.shouldAutoSync = false; - } - }); + // // disable auto sync on all wallets that aren't active/current + // ref.read(pWallets).wallets.forEach((e) { + // if (e.walletId != ref.read(currentWalletIdProvider)) { + // e.shouldAutoSync = false; + // } + // }); } }, child: Container( @@ -174,11 +173,11 @@ class SyncingOptionsView extends ConsumerWidget { ref.read(prefsChangeNotifierProvider).syncType = SyncingType.allWalletsOnStartup; - // enable auto sync on all wallets - ref - .read(pWallets) - .wallets - .forEach((e) => e.shouldAutoSync = true); + // // enable auto sync on all wallets + // ref + // .read(pWallets) + // .wallets + // .forEach((e) => e.shouldAutoSync = true); } }, child: Container( @@ -252,13 +251,13 @@ class SyncingOptionsView extends ConsumerWidget { ref.read(prefsChangeNotifierProvider).syncType = SyncingType.selectedWalletsAtStartup; - final ids = ref - .read(prefsChangeNotifierProvider) - .walletIdsSyncOnStartup; - - // enable auto sync on selected wallets only - ref.read(pWallets).wallets.forEach( - (e) => e.shouldAutoSync = ids.contains(e.walletId)); + // final ids = ref + // .read(prefsChangeNotifierProvider) + // .walletIdsSyncOnStartup; + // + // // enable auto sync on selected wallets only + // ref.read(pWallets).wallets.forEach( + // (e) => e.shouldAutoSync = ids.contains(e.walletId)); } }, child: Container( diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart index 824656496..8ded05dd6 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart @@ -13,13 +13,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/providers/global/active_wallet_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; @@ -181,9 +179,9 @@ class WalletSyncingOptionsView extends ConsumerWidget { .walletIdsSyncOnStartup)) .contains(info.walletId), onValueChanged: (value) { - final syncType = ref - .read(prefsChangeNotifierProvider) - .syncType; + // final syncType = ref + // .read(prefsChangeNotifierProvider) + // .syncType; final ids = ref .read(prefsChangeNotifierProvider) .walletIdsSyncOnStartup @@ -194,25 +192,25 @@ class WalletSyncingOptionsView extends ConsumerWidget { ids.remove(info.walletId); } - final wallet = ref - .read(pWallets) - .getWallet(info.walletId); - - switch (syncType) { - case SyncingType.currentWalletOnly: - if (info.walletId == - ref.read( - currentWalletIdProvider)) { - wallet.shouldAutoSync = value; - } - break; - case SyncingType - .selectedWalletsAtStartup: - case SyncingType - .allWalletsOnStartup: - wallet.shouldAutoSync = value; - break; - } + // final wallet = ref + // .read(pWallets) + // .getWallet(info.walletId); + // + // switch (syncType) { + // case SyncingType.currentWalletOnly: + // if (info.walletId == + // ref.read( + // currentWalletIdProvider)) { + // wallet.shouldAutoSync = value; + // } + // break; + // case SyncingType + // .selectedWalletsAtStartup: + // case SyncingType + // .allWalletsOnStartup: + // wallet.shouldAutoSync = value; + // break; + // } ref .read(prefsChangeNotifierProvider) diff --git a/lib/pages/settings_views/sub_widgets/settings_list_button.dart b/lib/pages/settings_views/sub_widgets/settings_list_button.dart index 2e4c19d01..62b4a2aec 100644 --- a/lib/pages/settings_views/sub_widgets/settings_list_button.dart +++ b/lib/pages/settings_views/sub_widgets/settings_list_button.dart @@ -16,17 +16,19 @@ import 'package:stackwallet/utilities/text_styles.dart'; class SettingsListButton extends StatelessWidget { const SettingsListButton({ - Key? key, + super.key, required this.iconAssetName, required this.title, this.onPressed, this.iconSize = 20.0, - }) : super(key: key); + this.padding = const EdgeInsets.all(8.0), + }); final String iconAssetName; final String title; final VoidCallback? onPressed; final double iconSize; + final EdgeInsetsGeometry padding; @override Widget build(BuildContext context) { @@ -44,7 +46,7 @@ class SettingsListButton extends StatelessWidget { ), onPressed: onPressed, child: Padding( - padding: const EdgeInsets.all(8.0), + padding: padding, child: Row( children: [ Container( diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart new file mode 100644 index 000000000..491f96c93 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart @@ -0,0 +1,181 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostMSWalletOptionsView extends ConsumerWidget { + const FrostMSWalletOptionsView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/frostMSWalletOptionsView"; + + final String walletId; + + static const _padding = 12.0; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "FROST Multisig options", + style: STextStyles.navBarTitle(context), + ), + ), + body: child), + ), + child: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: SettingsListButton( + padding: const EdgeInsets.all(_padding), + title: "Show participants", + iconAssetName: Assets.svg.peers, + onPressed: () { + Navigator.of(context).pushNamed( + FrostParticipantsView.routeName, + arguments: walletId, + ); + }, + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: SettingsListButton( + padding: const EdgeInsets.all(_padding), + title: "Initiate resharing", + iconAssetName: Assets.svg.swap2, + onPressed: () { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; + + ref.read(pFrostMyName.state).state = frostInfo.myName; + + Navigator.of(context).pushNamed( + InitiateResharingView.routeName, + arguments: walletId, + ); + }, + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: SettingsListButton( + padding: const EdgeInsets.all(_padding), + title: "Import reshare config", + iconAssetName: Assets.svg.downloadFolder, + iconSize: 16, + onPressed: () { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; + + ref.read(pFrostMyName.state).state = frostInfo.myName; + + final wallet = ref.read(pWallets).getWallet(walletId) + as BitcoinFrostWallet; + + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: wallet.walletId, + stepRoutes: FrostRouteGenerator.importReshareStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.resharing, + callerRouteName: FrostMSWalletOptionsView.routeName, + ); + + Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart new file mode 100644 index 000000000..364859ef0 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostParticipantsView extends ConsumerWidget { + const FrostParticipantsView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/frostParticipantsView"; + + final String walletId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(walletId)!; + + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Participants", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, + children: [ + for (int i = 0; i < frostInfo.participants.length; i++) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 5, + ), + child: RoundedWhiteContainer( + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldActiveBG, + borderRadius: BorderRadius.circular( + 200, + ), + ), + child: Center( + child: SvgPicture.asset( + Assets.svg.user, + width: 16, + height: 16, + ), + ), + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Text( + frostInfo.participants[i] == frostInfo.myName + ? "${frostInfo.participants[i]} (me)" + : frostInfo.participants[i], + style: STextStyles.w500_14(context), + ), + ), + const SizedBox( + width: 8, + ), + IconCopyButton( + data: frostInfo.participants[i], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart new file mode 100644 index 000000000..5ac986a6c --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart @@ -0,0 +1,499 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frostdart/frostdart.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +final class CompleteReshareConfigView extends ConsumerStatefulWidget { + const CompleteReshareConfigView({ + super.key, + required this.walletId, + required this.resharers, + }); + + static const String routeName = "/completeReshareConfigView"; + + final String walletId; + final Map resharers; + + @override + ConsumerState createState() => + _CompleteReshareConfigViewState(); +} + +class _CompleteReshareConfigViewState + extends ConsumerState { + final _newThresholdController = TextEditingController(); + final _newParticipantsCountController = TextEditingController(); + + final List controllers = []; + + late final String myName; + + int _participantsCount = 0; + + bool _buttonLock = false; + bool _includeMeInReshare = false; + + Future _onPressed() async { + if (_buttonLock) { + return; + } + _buttonLock = true; + + try { + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(widget.walletId)!; + final validationMessage = _validateInputData(); + + if (validationMessage != "valid") { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: validationMessage, + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + + final List newParticipants = + controllers.map((e) => e.text.trim()).toList(); + if (_includeMeInReshare) { + newParticipants.insert(0, myName); + } + + final config = Frost.createResharerConfig( + newThreshold: int.parse(_newThresholdController.text), + resharers: widget.resharers.values.toList(), + newParticipants: newParticipants, + ); + + final salt = Format.uint8listToString( + resharerSalt(resharerConfig: config), + ); + + if (frostInfo.knownSalts.contains(salt)) { + return await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Duplicate config salt", + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } else { + final salts = frostInfo.knownSalts; // Fixed length list. + final newSalts = List.from(salts)..add(salt); + final mainDB = ref.read(mainDBProvider); + await mainDB.isar.writeTxn(() async { + final info = frostInfo; + await mainDB.isar.frostWalletInfo.delete(info.id); + await mainDB.isar.frostWalletInfo.put( + info.copyWith(knownSalts: newSalts), + ); + }); + } + + ref.read(pFrostResharingData).myName = myName; + ref.read(pFrostResharingData).resharerRConfig = Frost.encodeRConfig( + config, + widget.resharers, + ); + + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet; + + if (mounted) { + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: wallet.walletId, + stepRoutes: FrostRouteGenerator.initiateReshareStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: FrostInterruptionDialogType.resharing, + callerRouteName: CompleteReshareConfigView.routeName, + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + } + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), + ); + } + } finally { + _buttonLock = false; + } + } + + String _validateInputData() { + final threshold = int.tryParse(_newThresholdController.text); + if (threshold == null) { + return "Choose a threshold"; + } + + final partsCount = int.tryParse(_newParticipantsCountController.text); + if (partsCount == null) { + return "Choose total number of participants"; + } + + if (threshold > partsCount) { + return "Threshold cannot be greater than the number of participants"; + } + + if (partsCount < 2) { + return "At least two participants required"; + } + + final newParticipants = controllers.map((e) => e.text.trim()).toList(); + + if (newParticipants.contains(myName)) { + return "Using your own name should be done using the checkbox to include" + " yourself"; + } + + if (_includeMeInReshare) { + newParticipants.add(myName); + } + + if (newParticipants.length != partsCount) { + return "Participants count error"; + } + + final hasEmptyParticipants = newParticipants + .map((e) => e.trim().isEmpty) + .reduce((value, element) => value |= element); + if (hasEmptyParticipants) { + return "Participants must not be empty"; + } + + if (newParticipants.length != newParticipants.toSet().length) { + return "Duplicate participant name found"; + } + + return "valid"; + } + + void _participantsCountChanged(String newValue) { + int? count = int.tryParse(newValue); + if (count != null) { + if (_includeMeInReshare) { + count = max(0, count - 1); + } + + if (count > _participantsCount) { + for (int i = _participantsCount; i < count; i++) { + controllers.add(TextEditingController()); + } + + _participantsCount = count; + setState(() {}); + } else if (count < _participantsCount) { + for (int i = _participantsCount; i > count; i--) { + final last = controllers.removeLast(); + last.dispose(); + } + + _participantsCount = count; + setState(() {}); + } + } + } + + @override + void initState() { + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(widget.walletId)!; + myName = frostInfo.myName; + super.initState(); + } + + @override + void dispose() { + _newThresholdController.dispose(); + _newParticipantsCountController.dispose(); + for (final e in controllers) { + e.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Edit group details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 8, + ), + GestureDetector( + onTap: () { + setState(() { + _includeMeInReshare = !_includeMeInReshare; + }); + _participantsCountChanged(_newParticipantsCountController.text); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 26, + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _includeMeInReshare, + onChanged: (value) { + setState( + () => _includeMeInReshare = value == true, + ); + _participantsCountChanged( + _newParticipantsCountController.text, + ); + }, + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "I will be a signer in the new config", + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 20, + ), + Text( + "New threshold", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), + ), + const SizedBox( + height: 10, + ), + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _newThresholdController, + decoration: InputDecoration( + hintText: "Enter number of signatures", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Text( + "Enter number of signatures required for fund management.", + style: STextStyles.w500_12(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle2, + ), + ), + ), + const SizedBox( + height: 16, + ), + Text( + "New number of participants", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), + ), + const SizedBox( + height: 10, + ), + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _newParticipantsCountController, + onChanged: _participantsCountChanged, + decoration: InputDecoration( + hintText: "Enter number of participants", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 6, + ), + RoundedWhiteContainer( + child: Text( + "The number of participants must be equal to or less than the" + " number of required signatures.", + style: STextStyles.w500_12(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle2, + ), + ), + ), + const SizedBox( + height: 16, + ), + if (controllers.isNotEmpty) + Text( + "Participants", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle1, + ), + ), + if (controllers.isNotEmpty) + const SizedBox( + height: 10, + ), + if (controllers.isNotEmpty) + RoundedWhiteContainer( + child: Text( + "Type each name in one word without spaces.", + style: STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + ), + ), + if (controllers.isNotEmpty) + Column( + children: [ + for (int i = 0; i < controllers.length; i++) + Padding( + padding: const EdgeInsets.only( + top: 10, + ), + child: TextField( + controller: controllers[i], + decoration: InputDecoration( + hintText: "Enter name", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Generate config", + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + } + await _onPressed(); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart new file mode 100644 index 000000000..ca5ab67e7 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart @@ -0,0 +1,258 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +final class InitiateResharingView extends ConsumerStatefulWidget { + const InitiateResharingView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/beginReshareConfigView"; + + final String walletId; + + @override + ConsumerState createState() => + _BeginReshareConfigViewState(); +} + +class _BeginReshareConfigViewState + extends ConsumerState { + late final String myName; + late final int currentThreshold; + late final List originalParticipants; + late final List currentParticipantsWithoutMe; + + final Set selectedParticipants = {}; + + @override + void initState() { + ref.read(pFrostResharingData).reset(); + + // TODO: optimize this by creating watcher providers (similar to normal WalletInfo) + final frostInfo = ref + .read(mainDBProvider) + .isar + .frostWalletInfo + .getByWalletIdSync(widget.walletId)!; + + currentThreshold = frostInfo.threshold; + originalParticipants = frostInfo.participants.toList(growable: false); + currentParticipantsWithoutMe = originalParticipants.toList(); + + // sanity check (should never actually fail, but very bad if it does) + assert(originalParticipants.length == currentParticipantsWithoutMe.length); + + myName = frostInfo.myName; + currentParticipantsWithoutMe.remove(myName); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) => DesktopScaffold( + background: Theme.of(context).extension()!.background, + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: SizedBox( + width: 480, + child: child, + ), + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Initiate resharing", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Select group members who will participate in resharing.", + style: STextStyles.w600_12(context), + ), + const SizedBox( + height: 10, + ), + Text( + "You must have the threshold number of members (including you) to initiate resharing.", + style: STextStyles.w600_12(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + Column( + children: [ + for (int i = 0; i < currentParticipantsWithoutMe.length; i++) + Padding( + padding: const EdgeInsets.only( + top: 10, + ), + child: RoundedWhiteContainer( + padding: EdgeInsets.zero, + onPressed: () { + if (selectedParticipants + .contains(currentParticipantsWithoutMe[i])) { + selectedParticipants + .remove(currentParticipantsWithoutMe[i]); + } else { + selectedParticipants + .add(currentParticipantsWithoutMe[i]); + } + + setState(() {}); + }, + child: Container( + color: Colors.transparent, + child: IgnorePointer( + child: Row( + children: [ + Checkbox( + value: selectedParticipants + .contains(currentParticipantsWithoutMe[i]), + onChanged: (_) {}, + ), + const SizedBox( + width: 10, + ), + Text( + currentParticipantsWithoutMe[i], + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ), + ), + ), + ], + ), + if (!Util.isDesktop) const Spacer(), + const SizedBox( + height: 16, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Required members", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + Text( + // +1 is included as the initiator who will also take part + "${selectedParticipants.length + 1} / $currentThreshold", + style: STextStyles.w500_14(context).copyWith( + color: selectedParticipants.length + 1 >= currentThreshold + ? Theme.of(context) + .extension()! + .accentColorGreen + : Theme.of(context) + .extension()! + .accentColorRed, + ), + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: "Continue", + // +1 is included as the initiator who will also take part + enabled: selectedParticipants.length + 1 >= currentThreshold, + onPressed: () async { + // include self now + selectedParticipants.add(myName); + + final Map resharers = {}; + + for (final name in selectedParticipants) { + resharers[name] = originalParticipants.indexOf(name); + } + + await Navigator.of(context).pushNamed( + CompleteReshareConfigView.routeName, + arguments: ( + walletId: widget.walletId, + resharers: resharers, + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart index 8c2873d0d..ab6baf455 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart @@ -17,34 +17,50 @@ import 'package:flutter_svg/svg.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/detail_item.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class WalletBackupView extends ConsumerWidget { const WalletBackupView({ - Key? key, + super.key, required this.walletId, required this.mnemonic, + this.frostWalletData, this.clipboardInterface = const ClipboardWrapper(), - }) : super(key: key); + }); static const String routeName = "/walletBackup"; final String walletId; final List mnemonic; + final ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? frostWalletData; final ClipboardInterface clipboardInterface; @override Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); + + final bool frost = frostWalletData != null; + final prevGen = frostWalletData?.prevGen != null; + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -91,139 +107,261 @@ class WalletBackupView extends ConsumerWidget { ), body: Padding( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 4, - ), - Text( - ref.watch(pWalletName(walletId)), - textAlign: TextAlign.center, - style: STextStyles.label(context).copyWith( - fontSize: 12, - ), - ), - const SizedBox( - height: 4, - ), - Text( - "Recovery Phrase", - textAlign: TextAlign.center, - style: STextStyles.pageTitleH1(context), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", - style: STextStyles.label(context), - ), - ), - ), - const SizedBox( - height: 8, - ), - Expanded( - child: SingleChildScrollView( - child: MnemonicTable( - words: mnemonic, - isDesktop: false, - ), - ), - ), - const SizedBox( - height: 12, - ), - TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - String data = AddressUtils.encodeQRSeedData(mnemonic); - - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (_) { - final width = MediaQuery.of(context).size.width / 2; - return StackDialogBase( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: Text( - "Recovery phrase QR code", - style: STextStyles.pageTitleH2(context), - ), - ), - const SizedBox( - height: 12, - ), - Center( - child: RepaintBoundary( - // key: _qrKey, - child: SizedBox( - width: width + 20, - height: width + 20, - child: QrImageView( - data: data, - size: width, - backgroundColor: Theme.of(context) - .extension()! - .popupBG, - foregroundColor: Theme.of(context) - .extension()! - .accentColorDark), + child: frost + ? LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Text( + "Please write down your backup data. Keep it safe and " + "never share it with anyone. " + "Your backup data is the only way you can access your " + "funds if you forget your PIN, lose your phone, etc." + "\n\n" + "Stack Wallet does not keep nor is able to restore " + "your backup data. " + "Only you have access to your wallet.", + style: STextStyles.label(context), ), ), - ), - const SizedBox( - height: 12, - ), - Center( - child: SizedBox( - width: width, - child: TextButton( - onPressed: () async { - // await _capturePng(true); - Navigator.of(context).pop(); - }, - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), + const SizedBox( + height: 24, + ), + // DetailItem( + // title: "My name", + // detail: frostWalletData!.myName, + // button: Util.isDesktop + // ? IconCopyButton( + // data: frostWalletData!.myName, + // ) + // : SimpleCopyButton( + // data: frostWalletData!.myName, + // ), + // ), + // const SizedBox( + // height: 16, + // ), + DetailItem( + title: "Multisig config", + detail: frostWalletData!.config, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.config, + ) + : SimpleCopyButton( + data: frostWalletData!.config, + ), + ), + const SizedBox( + height: 16, + ), + DetailItem( + title: "Keys", + detail: frostWalletData!.keys, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.keys, + ) + : SimpleCopyButton( + data: frostWalletData!.keys, + ), + ), + if (prevGen) + const SizedBox( + height: 24, + ), + if (prevGen) + RoundedWhiteContainer( child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + "Previous generation info", + style: STextStyles.label(context), ), ), - ), - ), - ], + if (prevGen) + const SizedBox( + height: 12, + ), + if (prevGen) + DetailItem( + title: "Previous multisig config", + detail: frostWalletData!.prevGen!.config, + button: Util.isDesktop + ? IconCopyButton( + data: + frostWalletData!.prevGen!.config, + ) + : SimpleCopyButton( + data: + frostWalletData!.prevGen!.config, + ), + ), + if (prevGen) + const SizedBox( + height: 16, + ), + if (prevGen) + DetailItem( + title: "Previous keys", + detail: frostWalletData!.prevGen!.keys, + button: Util.isDesktop + ? IconCopyButton( + data: frostWalletData!.prevGen!.keys, + ) + : SimpleCopyButton( + data: frostWalletData!.prevGen!.keys, + ), + ), + ], + ), ), - ); - }, - ); - }, - child: Text( - "Show QR Code", - style: STextStyles.button(context), + ), + ); + }, + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 4, + ), + Text( + ref.watch(pWalletName(walletId)), + textAlign: TextAlign.center, + style: STextStyles.label(context).copyWith( + fontSize: 12, + ), + ), + const SizedBox( + height: 4, + ), + Text( + "Recovery Phrase", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + color: + Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", + style: STextStyles.label(context), + ), + ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: SingleChildScrollView( + child: MnemonicTable( + words: mnemonic, + isDesktop: false, + ), + ), + ), + const SizedBox( + height: 12, + ), + TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + String data = AddressUtils.encodeQRSeedData(mnemonic); + + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (_) { + final width = MediaQuery.of(context).size.width / 2; + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + "Recovery phrase QR code", + style: STextStyles.pageTitleH2(context), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: RepaintBoundary( + // key: _qrKey, + child: SizedBox( + width: width + 20, + height: width + 20, + child: QrImageView( + data: data, + size: width, + backgroundColor: Theme.of(context) + .extension()! + .popupBG, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + const SizedBox( + height: 12, + ), + Center( + child: SizedBox( + width: width, + child: TextButton( + onPressed: () async { + // await _capturePng(true); + Navigator.of(context).pop(); + }, + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle( + context), + child: Text( + "Cancel", + style: STextStyles.button(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + child: Text( + "Show QR Code", + style: STextStyles.button(context), + ), + ), + ], ), - ), - ], - ), ), ), ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 0714528e0..66dfb8c23 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -22,6 +22,7 @@ import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; @@ -39,6 +40,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -51,13 +53,13 @@ import 'package:tuple/tuple.dart'; /// [eventBus] should only be set during testing class WalletSettingsView extends ConsumerStatefulWidget { const WalletSettingsView({ - Key? key, + super.key, required this.walletId, required this.coin, required this.initialSyncStatus, required this.initialNodeStatus, this.eventBus, - }) : super(key: key); + }); static const String routeName = "/walletSettings"; @@ -204,6 +206,22 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), + if (coin.isFrost) + const SizedBox( + height: 8, + ), + if (coin.isFrost) + SettingsListButton( + iconAssetName: Assets.svg.addressBook2, + iconSize: 16, + title: "FROST Multisig settings", + onPressed: () { + Navigator.of(context).pushNamed( + FrostMSWalletOptionsView.routeName, + arguments: walletId, + ); + }, + ), const SizedBox( height: 8, ), @@ -235,39 +253,79 @@ class _WalletSettingsViewState extends ConsumerState { final wallet = ref .read(pWallets) .getWallet(widget.walletId); - // TODO: [prio=frost] take wallets that don't have a mnemonic into account - if (wallet is MnemonicInterface) { - final mnemonic = - await wallet.getMnemonicAsWords(); - if (mounted) { - await Navigator.push( - context, - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: (_) => LockscreenView( - routeOnSuccessArguments: - Tuple2( - walletId, mnemonic), - showBackButton: true, - routeOnSuccess: - WalletBackupView - .routeName, - biometricsCancelButtonString: - "CANCEL", - biometricsLocalizedReason: - "Authenticate to view recovery phrase", - biometricsAuthenticationTitle: - "View recovery phrase", - ), - settings: const RouteSettings( - name: - "/viewRecoverPhraseLockscreen"), - ), + // TODO: [prio=med] take wallets that don't have a mnemonic into account + + List? mnemonic; + ({ + String myName, + String config, + String keys, + ({ + String config, + String keys + })? prevGen, + })? frostWalletData; + if (wallet is BitcoinFrostWallet) { + final futures = [ + wallet.getSerializedKeys(), + wallet.getMultisigConfig(), + wallet.getSerializedKeysPrevGen(), + wallet.getMultisigConfigPrevGen(), + ]; + + final results = + await Future.wait(futures); + + if (results.length == 4) { + frostWalletData = ( + myName: wallet.frostInfo.myName, + config: results[1]!, + keys: results[0]!, + prevGen: results[2] == null || + results[3] == null + ? null + : ( + config: results[3]!, + keys: results[2]!, + ), ); } + } else if (wallet + is MnemonicInterface) { + mnemonic = + await wallet.getMnemonicAsWords(); + } + + if (context.mounted) { + await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: (_) => LockscreenView( + routeOnSuccessArguments: ( + walletId: walletId, + mnemonic: mnemonic ?? [], + frostWalletData: + frostWalletData, + ), + showBackButton: true, + routeOnSuccess: + WalletBackupView.routeName, + biometricsCancelButtonString: + "CANCEL", + biometricsLocalizedReason: + "Authenticate to view recovery phrase", + biometricsAuthenticationTitle: + "View recovery phrase", + ), + settings: const RouteSettings( + name: + "/viewRecoverPhraseLockscreen"), + ), + ); } }, ); diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart index 0876c92e0..03b131993 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart @@ -83,7 +83,7 @@ class _ChangeRepresentativeViewState whileFuture: changeFuture(_textController.text), context: context, message: "Updating representative...", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, onException: (ex) { String msg = ex.toString(); while (msg.isNotEmpty && msg.startsWith("Exception:")) { diff --git a/lib/pages/special/firo_rescan_recovery_error_dialog.dart b/lib/pages/special/firo_rescan_recovery_error_dialog.dart index a812c377d..6f23c7a87 100644 --- a/lib/pages/special/firo_rescan_recovery_error_dialog.dart +++ b/lib/pages/special/firo_rescan_recovery_error_dialog.dart @@ -23,7 +23,6 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; -import 'package:tuple/tuple.dart'; enum FiroRescanRecoveryErrorViewOption { retry, @@ -269,8 +268,10 @@ class _FiroRescanRecoveryErrorViewState shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => LockscreenView( - routeOnSuccessArguments: - Tuple2(widget.walletId, mnemonic), + routeOnSuccessArguments: ( + walletId: widget.walletId, + mnemonic: mnemonic, + ), showBackButton: true, routeOnSuccess: WalletBackupView.routeName, biometricsCancelButtonString: "CANCEL", diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 003fd515a..2fd42300d 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -98,7 +98,7 @@ class _MyTokenSelectItemState extends ConsumerState { final success = await showLoading( whileFuture: _loadTokenWallet(context, ref), context: context, - isDesktop: isDesktop, + rootNavigator: isDesktop, message: "Loading ${widget.token.name}", ); diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index 45483e9d0..af0eca1c6 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -33,9 +33,9 @@ enum _BalanceType { class WalletBalanceToggleSheet extends ConsumerWidget { const WalletBalanceToggleSheet({ - Key? key, + super.key, required this.walletId, - }) : super(key: key); + }); final String walletId; @@ -46,7 +46,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { final coin = ref.watch(pWalletCoin(walletId)); final isFiro = coin == Coin.firo || coin == Coin.firoTestNet; - Balance balance = ref.watch(pWalletBalance(walletId)); + final balance = ref.watch(pWalletBalance(walletId)); _BalanceType _bal = ref.watch(walletBalanceToggleStateProvider.state).state == @@ -77,6 +77,11 @@ class WalletBalanceToggleSheet extends ConsumerWidget { // already set above break; } + + // hack to not show lelantus balance in ui if zero + if (balanceSecondary?.spendable.raw == BigInt.zero) { + balanceSecondary = null; + } } return Container( @@ -289,7 +294,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { class BalanceSelector extends ConsumerWidget { const BalanceSelector({ - Key? key, + super.key, required this.title, required this.coin, required this.balance, @@ -297,7 +302,7 @@ class BalanceSelector extends ConsumerWidget { required this.onChanged, required this.value, required this.groupValue, - }) : super(key: key); + }); final String title; final Coin coin; diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index 1a1513c1d..10d34771c 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -11,7 +11,6 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/transaction_filter.dart'; import 'package:stackwallet/providers/global/locale_provider.dart'; @@ -29,6 +28,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/date_picker/date_picker.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -40,9 +40,9 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class TransactionSearchFilterView extends ConsumerStatefulWidget { const TransactionSearchFilterView({ - Key? key, + super.key, required this.coin, - }) : super(key: key); + }); static const String routeName = "/transactionSearchFilter"; @@ -137,56 +137,6 @@ class _TransactionSearchViewState DateTime? _selectedFromDate = DateTime(2007); DateTime? _selectedToDate = DateTime.now(); - MaterialRoundedDatePickerStyle _buildDatePickerStyle() { - return MaterialRoundedDatePickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - // backgroundHeader: Theme.of(context).extension()!.textSubtitle2, - paddingMonthHeader: const EdgeInsets.only(top: 11), - colorArrowNext: Theme.of(context).extension()!.textSubtitle1, - colorArrowPrevious: - Theme.of(context).extension()!.textSubtitle1, - textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( - color: baseColor, - ), - textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), - textStyleDayHeader: STextStyles.datePicker600(context), - textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( - color: baseColor, - ), - textStyleDayOnCalendarDisabled: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle3, - ), - textStyleDayOnCalendarSelected: - STextStyles.datePicker400(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textStyleYearButton: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textWhite, - ), - // textStyleButtonAction: GoogleFonts.inter(), - ); - } - - MaterialRoundedYearPickerStyle _buildYearPickerStyle() { - return MaterialRoundedYearPickerStyle( - backgroundPicker: Theme.of(context).extension()!.popupBG, - textStyleYear: STextStyles.datePicker600(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle2, - fontSize: 16, - ), - textStyleYearSelected: STextStyles.datePicker600(context).copyWith( - fontSize: 18, - ), - ); - } - Widget _buildDateRangePicker() { const middleSeparatorPadding = 2.0; const middleSeparatorWidth = 12.0; @@ -207,58 +157,36 @@ class _TransactionSearchViewState child: GestureDetector( key: const Key("transactionSearchViewFromDatePickerKey"), onTap: () async { - final color = - Theme.of(context).extension()!.accentColorDark; - final height = MediaQuery.of(context).size.height; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 125)); } - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - initialDate: DateTime.now(), - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, + if (mounted) { + final date = await showSWDatePicker(context); + if (date != null) { + _selectedFromDate = date; - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _selectedFromDate = date; - - // flag to adjust date so from date is always before to date - final flag = _selectedToDate != null && - !_selectedFromDate!.isBefore(_selectedToDate!); - if (flag) { - _selectedToDate = DateTime.fromMillisecondsSinceEpoch( - _selectedFromDate!.millisecondsSinceEpoch); - } - - setState(() { + // flag to adjust date so from date is always before to date + final flag = _selectedToDate != null && + !_selectedFromDate!.isBefore(_selectedToDate!); if (flag) { - _toDateString = _selectedToDate == null - ? "" - : Format.formatDate(_selectedToDate!); + _selectedToDate = DateTime.fromMillisecondsSinceEpoch( + _selectedFromDate!.millisecondsSinceEpoch); } - _fromDateString = _selectedFromDate == null - ? "" - : Format.formatDate(_selectedFromDate!); - }); + + setState(() { + if (flag) { + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); + } + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + }); + } } }, child: Container( @@ -319,58 +247,36 @@ class _TransactionSearchViewState child: GestureDetector( key: const Key("transactionSearchViewToDatePickerKey"), onTap: () async { - final color = - Theme.of(context).extension()!.accentColorDark; - final height = MediaQuery.of(context).size.height; // check and hide keyboard if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 125)); } - final date = await showRoundedDatePicker( - // This doesn't change statusbar color... - // background: CFColors.starryNight.withOpacity(0.8), - context: context, - height: height * 0.5, - theme: ThemeData( - primarySwatch: Util.createMaterialColor( - color, - ), - ), - //TODO pick a better initial date - // 2007 chosen as that is just before bitcoin launched - initialDate: DateTime.now(), - firstDate: DateTime(2007), - lastDate: DateTime.now(), - borderRadius: Constants.size.circularBorderRadius * 2, + if (mounted) { + final date = await showSWDatePicker(context); + if (date != null) { + _selectedToDate = date; - textPositiveButton: "SELECT", - - styleDatePicker: _buildDatePickerStyle(), - styleYearPicker: _buildYearPickerStyle(), - ); - if (date != null) { - _selectedToDate = date; - - // flag to adjust date so from date is always before to date - final flag = _selectedFromDate != null && - !_selectedToDate!.isAfter(_selectedFromDate!); - if (flag) { - _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( - _selectedToDate!.millisecondsSinceEpoch); - } - - setState(() { + // flag to adjust date so from date is always before to date + final flag = _selectedFromDate != null && + !_selectedToDate!.isAfter(_selectedFromDate!); if (flag) { - _fromDateString = _selectedFromDate == null - ? "" - : Format.formatDate(_selectedFromDate!); + _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( + _selectedToDate!.millisecondsSinceEpoch); } - _toDateString = _selectedToDate == null - ? "" - : Format.formatDate(_selectedToDate!); - }); + + setState(() { + if (flag) { + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + } + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); + }); + } } }, child: Container( @@ -454,7 +360,7 @@ class _TransactionSearchViewState FocusScope.of(context).unfocus(); await Future.delayed(const Duration(milliseconds: 75)); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -908,7 +814,7 @@ class _TransactionSearchViewState ); } } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart index f47417d99..ac868aee9 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart @@ -23,6 +23,7 @@ import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; @@ -44,6 +45,7 @@ class _TransactionsV2ListState extends ConsumerState { late final StreamSubscription> _subscription; late final Query _query; + late final Coin coin; BorderRadius get _borderRadiusFirst { return BorderRadius.only( @@ -69,6 +71,7 @@ class _TransactionsV2ListState extends ConsumerState { @override void initState() { + coin = ref.read(pWallets).getWallet(widget.walletId).info.coin; _query = ref .read(mainDBProvider) .isar @@ -110,8 +113,6 @@ class _TransactionsV2ListState extends ConsumerState { @override Widget build(BuildContext context) { - final coin = ref.watch(pWallets).getWallet(widget.walletId).info.coin; - return FutureBuilder( future: _query.findAll(), builder: (fbContext, AsyncSnapshot> snapshot) { diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 07d592b33..f00751425 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -16,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/frost_route_generator.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; @@ -29,6 +30,7 @@ import 'package:stackwallet/pages/ordinals/ordinals_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; @@ -64,6 +66,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; @@ -76,12 +79,14 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/small_tor_icon.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/fusion_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart'; @@ -119,7 +124,7 @@ class _WalletViewState extends ConsumerState { late final bool isSparkWallet; - late final bool _shouldDisableAutoSyncOnLogOut; + // late final bool _shouldDisableAutoSyncOnLogOut; late WalletSyncStatus _currentSyncStatus; late NodeConnectionStatus _currentNodeStatus; @@ -165,14 +170,16 @@ class _WalletViewState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(walletId); coin = wallet.info.coin; - ref.read(currentWalletIdProvider.notifier).state = wallet.walletId; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(currentWalletIdProvider.notifier).state = wallet.walletId; + }); if (!wallet.shouldAutoSync) { // enable auto sync if it wasn't enabled when loading wallet wallet.shouldAutoSync = true; - _shouldDisableAutoSyncOnLogOut = true; - } else { - _shouldDisableAutoSyncOnLogOut = false; + // _shouldDisableAutoSyncOnLogOut = true; + // } else { + // _shouldDisableAutoSyncOnLogOut = false; } isSparkWallet = wallet is SparkInterface; @@ -270,34 +277,36 @@ class _WalletViewState extends ConsumerState { const timeout = Duration(milliseconds: 1500); if (_cachedTime == null || now.difference(_cachedTime!) > timeout) { _cachedTime = now; - unawaited(showDialog( - context: context, - barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async { - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName), - ); - _logout(); - return false; - }, - child: const StackDialog(title: "Tap back again to exit wallet"), + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async { + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName), + ); + _logout(); + return false; + }, + child: const StackDialog(title: "Tap back again to exit wallet"), + ), + ).timeout( + timeout, + onTimeout: () => Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName), + ), ), - ).timeout( - timeout, - onTimeout: () => Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ), - )); + ); } return false; } void _logout() async { - if (_shouldDisableAutoSyncOnLogOut) { - // disable auto sync if it was enabled only when loading wallet - ref.read(pWallets).getWallet(walletId).shouldAutoSync = false; - } + // if (_shouldDisableAutoSyncOnLogOut) { + // // disable auto sync if it was enabled only when loading wallet + ref.read(pWallets).getWallet(walletId).shouldAutoSync = false; + // } ref.read(currentWalletIdProvider.notifier).state = null; ref.read(transactionFilterProvider.state).state = null; @@ -354,7 +363,27 @@ class _WalletViewState extends ConsumerState { } } - void _onExchangePressed(BuildContext context) async { + Future _onFrostSignPressed(BuildContext context) async { + final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet; + ref.read(pFrostScaffoldArgs.state).state = ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: walletId, + stepRoutes: FrostRouteGenerator.signFrostTxStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType.transactionCreation, + callerRouteName: WalletView.routeName, + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + } + + Future _onExchangePressed(BuildContext context) async { final Coin coin = ref.read(pWalletCoin(walletId)); if (coin.isTestNet) { @@ -372,11 +401,12 @@ class _WalletViewState extends ConsumerState { .tickerEqualToAnyExchangeNameName(coin.ticker) .findFirst(); } catch (_) { - _future = ExchangeDataLoadingService.instance.loadAll().then((_) => - ExchangeDataLoadingService.instance.isar.currencies - .where() - .tickerEqualToAnyExchangeNameName(coin.ticker) - .findFirst()); + _future = ExchangeDataLoadingService.instance.loadAll().then( + (_) => ExchangeDataLoadingService.instance.isar.currencies + .where() + .tickerEqualToAnyExchangeNameName(coin.ticker) + .findFirst(), + ); } final currency = await showLoading( @@ -385,7 +415,7 @@ class _WalletViewState extends ConsumerState { message: "Loading...", ); - if (mounted) { + if (context.mounted) { unawaited( Navigator.of(context).pushNamed( WalletInitiatedExchangeView.routeName, @@ -399,7 +429,7 @@ class _WalletViewState extends ConsumerState { } } - void _onBuyPressed(BuildContext context) async { + Future _onBuyPressed(BuildContext context) async { final Coin coin = ref.read(pWalletCoin(walletId)); if (coin.isTestNet) { @@ -542,7 +572,7 @@ class _WalletViewState extends ConsumerState { }, ), ), - ) + ), ], ), ); @@ -581,7 +611,7 @@ class _WalletViewState extends ConsumerState { style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), - ) + ), ], ), actions: [ @@ -644,9 +674,12 @@ class _WalletViewState extends ConsumerState { color: Theme.of(context) .extension()! .background, - icon: ref.watch(notificationsProvider.select( - (value) => value - .hasUnreadNotificationsFor(walletId))) + icon: ref.watch( + notificationsProvider.select( + (value) => + value.hasUnreadNotificationsFor(walletId), + ), + ) ? SvgPicture.file( File( ref.watch( @@ -657,10 +690,14 @@ class _WalletViewState extends ConsumerState { ), width: 20, height: 20, - color: ref.watch(notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor( - walletId))) + color: ref.watch( + notificationsProvider.select( + (value) => + value.hasUnreadNotificationsFor( + walletId, + ), + ), + ) ? null : Theme.of(context) .extension()! @@ -670,10 +707,14 @@ class _WalletViewState extends ConsumerState { Assets.svg.bell, width: 20, height: 20, - color: ref.watch(notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor( - walletId))) + color: ref.watch( + notificationsProvider.select( + (value) => + value.hasUnreadNotificationsFor( + walletId, + ), + ), + ) ? null : Theme.of(context) .extension()! @@ -694,22 +735,25 @@ class _WalletViewState extends ConsumerState { .state; if (unreadNotificationIds.isEmpty) return; - List> futures = []; + final List> futures = []; for (int i = 0; i < unreadNotificationIds.length - 1; i++) { - futures.add(ref - .read(notificationsProvider) - .markAsRead( + futures.add( + ref.read(notificationsProvider).markAsRead( unreadNotificationIds.elementAt(i), - false)); + false, + ), + ); } // wait for multiple to update if any Future.wait(futures).then((_) { // only notify listeners once ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.last, true); + unreadNotificationIds.last, + true, + ); }); }); }, @@ -798,7 +842,8 @@ class _WalletViewState extends ConsumerState { style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle( - context), + context, + ), onPressed: () async { await showDialog( context: context, @@ -829,7 +874,8 @@ class _WalletViewState extends ConsumerState { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle( - context), + context, + ), child: Text( "Continue", style: @@ -974,6 +1020,12 @@ class _WalletViewState extends ConsumerState { } }, ), + if (ref.watch(pWalletCoin(walletId)).isFrost) + WalletNavigationBarItemData( + label: "Sign", + icon: const FrostSignNavIcon(), + onTap: () => _onFrostSignPressed(context), + ), WalletNavigationBarItemData( label: "Send", icon: const SendNavIcon(), @@ -994,21 +1046,26 @@ class _WalletViewState extends ConsumerState { // break; // } Navigator.of(context).pushNamed( - SendView.routeName, - arguments: Tuple2( - walletId, - coin, + ref.read(pWallets).getWallet(walletId) + is BitcoinFrostWallet + ? FrostSendView.routeName + : SendView.routeName, + arguments: ( + walletId: walletId, + coin: coin, ), ); }, ), - if (Constants.enableExchange) + if (Constants.enableExchange && + !ref.watch(pWalletCoin(walletId)).isFrost) WalletNavigationBarItemData( label: "Swap", icon: const ExchangeNavIcon(), onTap: () => _onExchangePressed(context), ), - if (Constants.enableExchange) + if (Constants.enableExchange && + !ref.watch(pWalletCoin(walletId)).isFrost) WalletNavigationBarItemData( label: "Buy", icon: const BuyNavIcon(), @@ -1036,21 +1093,22 @@ class _WalletViewState extends ConsumerState { ), if (coin == Coin.banano) WalletNavigationBarItemData( - icon: SvgPicture.asset( - Assets.svg.monkey, - height: 20, - width: 20, - color: Theme.of(context) - .extension()! - .bottomNavIconIcon, - ), - label: "MonKey", - onTap: () { - Navigator.of(context).pushNamed( - MonkeyView.routeName, - arguments: widget.walletId, - ); - }), + icon: SvgPicture.asset( + Assets.svg.monkey, + height: 20, + width: 20, + color: Theme.of(context) + .extension()! + .bottomNavIconIcon, + ), + label: "MonKey", + onTap: () { + Navigator.of(context).pushNamed( + MonkeyView.routeName, + arguments: widget.walletId, + ); + }, + ), if (ref.watch( pWallets.select( (value) => value.getWallet(widget.walletId) @@ -1075,8 +1133,12 @@ class _WalletViewState extends ConsumerState { ); }, ), - if (ref.watch(pWallets.select((value) => - value.getWallet(widget.walletId) is PaynymInterface))) + if (ref.watch( + pWallets.select( + (value) => + value.getWallet(widget.walletId) is PaynymInterface, + ), + )) WalletNavigationBarItemData( label: "PayNym", icon: const PaynymNavIcon(), @@ -1108,7 +1170,7 @@ class _WalletViewState extends ConsumerState { level: LogLevel.Info, ); - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); // check if account exists and for matching code to see if claimed diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 8aafe8e30..4ef59bf0b 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -127,7 +127,7 @@ class _FavoriteCardState extends ConsumerState { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (mounted) { diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 194b8df10..a708267a9 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -17,6 +17,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_overview.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/supported_coins.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -26,6 +27,7 @@ import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; +import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletListItem extends ConsumerWidget { @@ -58,6 +60,25 @@ class WalletListItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () async { + // Check if Tor is enabled... + if (ref.read(prefsChangeNotifierProvider).useTor) { + // ... and if the coin supports Tor. + final cryptocurrency = SupportedCoins.getCryptoCurrencyFor(coin); + if (!cryptocurrency.torSupport) { + // If not, show a Tor warning dialog. + final shouldContinue = await showDialog( + context: context, + builder: (_) => TorWarningDialog( + coin: coin, + ), + ) ?? + false; + if (!shouldContinue) { + return; + } + } + } + if (walletCount == 1 && coin != Coin.ethereum) { final wallet = ref .read(pWallets) @@ -74,7 +95,7 @@ class WalletListItem extends ConsumerWidget { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (context.mounted) { unawaited( diff --git a/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart index d5390e3fd..aba315c64 100644 --- a/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart +++ b/lib/pages_desktop_specific/address_book_view/desktop_address_book.dart @@ -11,11 +11,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_address_book_scaffold.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_book_filter_provider.dart'; @@ -25,6 +27,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/address_book_card.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -38,7 +41,7 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; class DesktopAddressBook extends ConsumerStatefulWidget { - const DesktopAddressBook({Key? key}) : super(key: key); + const DesktopAddressBook({super.key}); static const String routeName = "/desktopAddressBook"; @@ -93,10 +96,11 @@ class _DesktopAddressBook extends ConsumerState { ref.refresh(addressBookFilterProvider); // if (widget.coin == null) { - List coins = Coin.values.toList(); + final List coins = Coin.values.toList(); coins.remove(Coin.firoTestNet); - bool showTestNet = ref.read(prefsChangeNotifierProvider).showTestNetCoins; + final bool showTestNet = + ref.read(prefsChangeNotifierProvider).showTestNetCoins; if (showTestNet) { ref.read(addressBookFilterProvider).addAll(coins, false); @@ -110,13 +114,26 @@ class _DesktopAddressBook extends ConsumerState { // } WidgetsBinding.instance.addPostFrameCallback((_) async { - List addresses = []; + final List addresses = []; final wallets = ref.read(pWallets).wallets; for (final wallet in wallets) { + final String addressString; + if (wallet is SparkInterface) { + Address? address = await wallet.getCurrentReceivingSparkAddress(); + if (address == null) { + address = await wallet.generateNextSparkAddress(); + await ref.read(mainDBProvider).updateOrPutAddresses([address]); + } + addressString = address.value; + } else { + final address = await wallet.getCurrentReceivingAddress(); + addressString = address?.value ?? wallet.info.cachedReceivingAddress; + } + addresses.add( ContactAddressEntry() ..coinName = wallet.info.coin.name - ..address = wallet.info.cachedReceivingAddress + ..address = addressString ..label = "Current Receiving" ..other = wallet.info.name, ); @@ -148,26 +165,41 @@ class _DesktopAddressBook extends ConsumerState { ref.watch(addressBookServiceProvider.select((value) => value.contacts)); final allContacts = contacts - .where((element) => - element.addresses.isEmpty || - element.addresses - .where((e) => ref.watch(addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)))) - .isNotEmpty) .where( - (e) => ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + (element) => + element.addresses.isEmpty || + element.addresses + .where( + (e) => ref.watch( + addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)), + ), + ) + .isNotEmpty, + ) + .where( + (e) => ref.read(addressBookServiceProvider).matches(_searchTerm, e), + ) .toList(); final favorites = contacts - .where((element) => - element.addresses.isEmpty || - element.addresses - .where((e) => ref.watch(addressBookFilterProvider - .select((value) => value.coins.contains(e.coin)))) - .isNotEmpty) - .where((e) => - e.isFavorite && - ref.read(addressBookServiceProvider).matches(_searchTerm, e)) + .where( + (element) => + element.addresses.isEmpty || + element.addresses + .where( + (e) => ref.watch( + addressBookFilterProvider + .select((value) => value.coins.contains(e.coin)), + ), + ) + .isNotEmpty, + ) + .where( + (e) => + e.isFavorite && + ref.read(addressBookServiceProvider).matches(_searchTerm, e), + ) .where((element) => element.isFavorite) .toList(); @@ -182,7 +214,7 @@ class _DesktopAddressBook extends ConsumerState { Text( "Address Book", style: STextStyles.desktopH3(context), - ) + ), ], ), ), @@ -354,7 +386,8 @@ class _DesktopAddressBook extends ConsumerState { ), child: AddressBookCard( key: Key( - "favContactCard_${favorites[i].customId}_key"), + "favContactCard_${favorites[i].customId}_key", + ), contactId: favorites[i].customId, desktopSendFrom: false, ), @@ -426,7 +459,8 @@ class _DesktopAddressBook extends ConsumerState { ), child: AddressBookCard( key: Key( - "favContactCard_${allContacts[i].customId}_key"), + "favContactCard_${allContacts[i].customId}_key", + ), contactId: allContacts[i].customId, desktopSendFrom: false, ), diff --git a/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart b/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart index 9a9591e6c..7b94575b4 100644 --- a/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart +++ b/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart @@ -29,10 +29,10 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class DesktopAddressList extends ConsumerStatefulWidget { const DesktopAddressList({ - Key? key, + super.key, required this.walletId, this.searchHeight, - }) : super(key: key); + }); final String walletId; final double? searchHeight; diff --git a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart index 2e02aeef9..6d4225fab 100644 --- a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart +++ b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart @@ -132,7 +132,7 @@ class _FusionDialogViewState extends ConsumerState { Future.delayed(const Duration(seconds: 2)), ]), context: context, - isDesktop: true, + rootNavigator: true, message: "Stopping fusion", ); diff --git a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart index 7f138509f..94cdf75f8 100644 --- a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart @@ -92,7 +92,7 @@ class CoinWalletsTable extends ConsumerWidget { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (context.mounted) { diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index 63b45d0c8..3b3c6f32f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -15,6 +15,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/wallets_view/wallets_overview.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/supported_coins.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -24,10 +25,11 @@ import 'package:stackwallet/wallets/isar/providers/all_wallets_info_provider.dar import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletSummaryTable extends ConsumerStatefulWidget { - const WalletSummaryTable({Key? key}) : super(key: key); + const WalletSummaryTable({super.key}); @override ConsumerState createState() => _WalletTableState(); @@ -68,10 +70,10 @@ class _WalletTableState extends ConsumerState { class DesktopWalletSummaryRow extends ConsumerStatefulWidget { const DesktopWalletSummaryRow({ - Key? key, + super.key, required this.coin, required this.walletCount, - }) : super(key: key); + }); final Coin coin; final int walletCount; @@ -85,7 +87,26 @@ class _DesktopWalletSummaryRowState extends ConsumerState { bool _hovering = false; - void _onPressed() { + void _onPressed() async { + // Check if Tor is enabled... + if (ref.read(prefsChangeNotifierProvider).useTor) { + // ... and if the coin supports Tor. + final cryptocurrency = SupportedCoins.getCryptoCurrencyFor(widget.coin); + if (!cryptocurrency.torSupport) { + // If not, show a Tor warning dialog. + final shouldContinue = await showDialog( + context: context, + builder: (_) => TorWarningDialog( + coin: widget.coin, + ), + ) ?? + false; + if (!shouldContinue) { + return; + } + } + } + showDialog( context: context, builder: (_) => DesktopDialog( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 70766661b..146f596d7 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -72,7 +72,7 @@ class _DesktopWalletViewState extends ConsumerState { late final TextEditingController controller; late final EventBus eventBus; - late final bool _shouldDisableAutoSyncOnLogOut; + // late final bool _shouldDisableAutoSyncOnLogOut; Future onBackPressed() async { await _logout(); @@ -83,10 +83,10 @@ class _DesktopWalletViewState extends ConsumerState { Future _logout() async { final wallet = ref.read(pWallets).getWallet(widget.walletId); - if (_shouldDisableAutoSyncOnLogOut) { - // disable auto sync if it was enabled only when loading wallet - wallet.shouldAutoSync = false; - } + // if (_shouldDisableAutoSyncOnLogOut) { + // // disable auto sync if it was enabled only when loading wallet + wallet.shouldAutoSync = false; + // } ref.read(transactionFilterProvider.state).state = null; if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && ref.read(prefsChangeNotifierProvider).backupFrequencyType == @@ -131,11 +131,11 @@ class _DesktopWalletViewState extends ConsumerState { ref.read(currentWalletIdProvider.notifier).state = wallet.walletId); if (!wallet.shouldAutoSync) { - // enable auto sync if it wasn't enabled when loading wallet + // // enable auto sync if it wasn't enabled when loading wallet wallet.shouldAutoSync = true; - _shouldDisableAutoSyncOnLogOut = true; - } else { - _shouldDisableAutoSyncOnLogOut = false; + // _shouldDisableAutoSyncOnLogOut = true; + // } else { + // _shouldDisableAutoSyncOnLogOut = false; } wallet.refresh(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart index b574d377e..aba28300b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart @@ -26,9 +26,9 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; class DesktopAuthSend extends ConsumerStatefulWidget { const DesktopAuthSend({ - Key? key, + super.key, required this.coin, - }) : super(key: key); + }); final Coin coin; @@ -43,11 +43,52 @@ class _DesktopAuthSendState extends ConsumerState { bool hidePassword = true; bool _confirmEnabled = false; + bool _lock = false; - Future verifyPassphrase() async { - return await ref - .read(storageCryptoHandlerProvider) - .verifyPassphrase(passwordController.text); + Future _confirmPressed() async { + if (_lock) { + return; + } + _lock = true; + + try { + unawaited( + showDialog( + context: context, + builder: (context) => const Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + LoadingIndicator( + width: 200, + height: 200, + ), + ], + ), + ), + ); + + await Future.delayed(const Duration(seconds: 1)); + + final passwordIsValid = await ref + .read(storageCryptoHandlerProvider) + .verifyPassphrase(passwordController.text); + + if (mounted) { + Navigator.of(context).pop(); + Navigator.of( + context, + rootNavigator: true, + ).pop(passwordIsValid); + await Future.delayed( + const Duration( + milliseconds: 100, + ), + ); + } + } finally { + _lock = false; + } } @override @@ -108,6 +149,12 @@ class _DesktopAuthSendState extends ConsumerState { obscureText: hidePassword, enableSuggestions: false, autocorrect: false, + autofocus: true, + onSubmitted: (_) { + if (_confirmEnabled) { + _confirmPressed(); + } + }, decoration: standardInputDecoration( "Enter password", passwordFocusNode, @@ -173,38 +220,7 @@ class _DesktopAuthSendState extends ConsumerState { enabled: _confirmEnabled, label: "Confirm", buttonHeight: ButtonHeight.l, - onPressed: () async { - unawaited( - showDialog( - context: context, - builder: (context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: const [ - LoadingIndicator( - width: 200, - height: 200, - ), - ], - ), - ), - ); - - await Future.delayed(const Duration(seconds: 1)); - - final passwordIsValid = await verifyPassphrase(); - - if (mounted) { - Navigator.of(context).pop(); - Navigator.of( - context, - rootNavigator: true, - ).pop(passwordIsValid); - await Future.delayed(const Duration( - milliseconds: 100, - )); - } - }, + onPressed: _confirmPressed, ), ), ], diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 90c5ae041..2614a58d4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -52,6 +52,7 @@ import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'package:stackwallet/widgets/animated_text.dart'; @@ -271,6 +272,11 @@ class _DesktopSendState extends ConsumerState { padding: const EdgeInsets.all(32), child: BuildingTransactionDialog( coin: wallet.info.coin, + isSpark: wallet is FiroWallet && + ref + .read(publicPrivateBalanceStateProvider.state) + .state == + FiroType.spark, onCancel: () { wasCancelled = true; @@ -305,7 +311,7 @@ class _DesktopSendState extends ConsumerState { address: widget.accountLite!.code, amount: amount, isChange: false, - ) + ), ], satsPerVByte: isCustomFee ? customFeeRate : null, feeRateType: feeRate, @@ -328,7 +334,7 @@ class _DesktopSendState extends ConsumerState { amount: amount, memo: memoController.text, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -347,7 +353,7 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -369,7 +375,7 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], ), ); @@ -385,7 +391,7 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], sparkRecipients: ref.read(pValidSparkSendToAddress) ? [ @@ -394,7 +400,7 @@ class _DesktopSendState extends ConsumerState { amount: amount, memo: memoController.text, isChange: false, - ) + ), ] : null, ), @@ -410,7 +416,7 @@ class _DesktopSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], memo: memo, feeRateType: ref.read(feeRateTypeStateProvider), @@ -576,8 +582,10 @@ class _DesktopSendState extends ConsumerState { if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { return; } - Logging.instance.log("it changed $amount $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $amount $_cachedAmountToSend", + level: LogLevel.Info, + ); _cachedAmountToSend = amount; final price = @@ -626,8 +634,10 @@ class _DesktopSendState extends ConsumerState { final qrResult = await scanner.scan(); - Logging.instance.log("qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); final results = AddressUtils.parseUri(qrResult.rawContent); @@ -678,8 +688,9 @@ class _DesktopSendState extends ConsumerState { // here we ignore the exception caused by not giving permission // to use the camera to scan a qr code Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } } @@ -733,7 +744,7 @@ class _DesktopSendState extends ConsumerState { } else { final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); if (data?.text != null && data!.text!.isNotEmpty) { - String content = data.text!.trim(); + final String content = data.text!.trim(); setState(() { memoController.text = content; @@ -864,7 +875,8 @@ class _DesktopSendState extends ConsumerState { if (isPaynymSend) { sendToController.text = widget.accountLite!.nymName; WidgetsBinding.instance.addPostFrameCallback( - (_) => _setValidAddressProviders(sendToController.text)); + (_) => _setValidAddressProviders(sendToController.text), + ); } _cryptoFocus.addListener(() { @@ -911,7 +923,8 @@ class _DesktopSendState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); final String locale = ref.watch( - localeServiceChangeNotifierProvider.select((value) => value.locale)); + localeServiceChangeNotifierProvider.select((value) => value.locale), + ); // add listener for epic cash to strip http:// and https:// prefixes if the address also ocntains an @ symbol (indicating an epicbox address) if (coin == Coin.epicCash) { @@ -974,9 +987,11 @@ class _DesktopSendState extends ConsumerState { width: 10, ), Text( - ref.watch(pAmountFormatter(coin)).format(ref - .watch(pWalletBalanceTertiary(walletId)) - .spendable), + ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pWalletBalanceTertiary(walletId)) + .spendable, + ), style: STextStyles.itemSubtitle(context), ), ], @@ -994,9 +1009,11 @@ class _DesktopSendState extends ConsumerState { width: 10, ), Text( - ref.watch(pAmountFormatter(coin)).format(ref - .watch(pWalletBalanceSecondary(walletId)) - .spendable), + ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pWalletBalanceSecondary(walletId)) + .spendable, + ), style: STextStyles.itemSubtitle(context), ), ], @@ -1015,7 +1032,8 @@ class _DesktopSendState extends ConsumerState { ), Text( ref.watch(pAmountFormatter(coin)).format( - ref.watch(pWalletBalance(walletId)).spendable), + ref.watch(pWalletBalance(walletId)).spendable, + ), style: STextStyles.itemSubtitle(context), ), ], @@ -1163,9 +1181,10 @@ class _DesktopSendState extends ConsumerState { child: Text( ref.watch(pAmountUnit(coin)).unitForCoin(coin), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1222,12 +1241,15 @@ class _DesktopSendState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(12), child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), + ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + ), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -1336,7 +1358,8 @@ class _DesktopSendState extends ConsumerState { _addressToggleFlag ? TextFieldIconButton( key: const Key( - "sendViewClearAddressFieldButtonKey"), + "sendViewClearAddressFieldButtonKey", + ), onTap: () { sendToController.text = ""; _address = ""; @@ -1349,7 +1372,8 @@ class _DesktopSendState extends ConsumerState { ) : TextFieldIconButton( key: const Key( - "sendViewPasteAddressFieldButtonKey"), + "sendViewPasteAddressFieldButtonKey", + ), onTap: pasteAddress, child: sendToController.text.isEmpty ? const ClipboardIcon() @@ -1379,7 +1403,8 @@ class _DesktopSendState extends ConsumerState { child: Text( "Address book", style: STextStyles.desktopH3( - context), + context, + ), ), ), const DesktopDialogCloseButton(), @@ -1435,7 +1460,9 @@ class _DesktopSendState extends ConsumerState { FiroType.lelantus) { if (_data != null && _data!.contactLabel == _address) { error = SparkInterface.validateSparkAddress( - address: _data!.address, isTestNet: coin.isTestNet) + address: _data!.address, + isTestNet: coin.isTestNet, + ) ? "Lelantus to Spark not supported" : null; } else if (ref.watch(pValidSparkSendToAddress)) { @@ -1566,7 +1593,8 @@ class _DesktopSendState extends ConsumerState { if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos] .contains(coin))) ConditionalParent( - condition: coin.isElectrumXCoin && + condition: ref.watch(pWallets).getWallet(walletId) + is ElectrumXInterface && !(((coin == Coin.firo || coin == Coin.firoTestNet) && (ref.watch(publicPrivateBalanceStateProvider.state).state == FiroType.lelantus || @@ -1660,8 +1688,9 @@ class _DesktopSendState extends ConsumerState { if (coin == Coin.monero || coin == Coin.wownero) { final fee = await wallet.estimateFeeFor( - amount, - MoneroTransactionPriority.regular.raw!); + amount, + MoneroTransactionPriority.regular.raw!, + ); ref .read(feeSheetSessionCacheProvider) .average[amount] = fee; @@ -1669,16 +1698,18 @@ class _DesktopSendState extends ConsumerState { coin == Coin.firoTestNet) && ref .read( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state != FiroType.public) { final firoWallet = wallet as FiroWallet; if (ref .read( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state == FiroType.lelantus) { ref @@ -1688,8 +1719,9 @@ class _DesktopSendState extends ConsumerState { .estimateFeeForLelantus(amount); } else if (ref .read( - publicPrivateBalanceStateProvider - .state) + publicPrivateBalanceStateProvider + .state, + ) .state == FiroType.spark) { ref @@ -1703,7 +1735,9 @@ class _DesktopSendState extends ConsumerState { .read(feeSheetSessionCacheProvider) .average[amount] = await wallet.estimateFeeFor( - amount, feeRate); + amount, + feeRate, + ); } } return ref @@ -1718,8 +1752,8 @@ class _DesktopSendState extends ConsumerState { AnimatedText( stringsToLoopThrough: stringsToLoopThrough, style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( + context, + ).copyWith( color: Theme.of(context) .extension()! .textFieldActiveText, @@ -1733,7 +1767,8 @@ class _DesktopSendState extends ConsumerState { : (coin == Coin.firo || coin == Coin.firoTestNet) && ref .watch( - publicPrivateBalanceStateProvider.state) + publicPrivateBalanceStateProvider.state, + ) .state == FiroType.lelantus ? Text( @@ -1758,8 +1793,8 @@ class _DesktopSendState extends ConsumerState { Text( feeSelectionResult?.$2 ?? "", style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( + context, + ).copyWith( color: Theme.of(context) .extension()! .textFieldActiveText, @@ -1769,8 +1804,8 @@ class _DesktopSendState extends ConsumerState { Text( feeSelectionResult?.$3 ?? "", style: STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( + context, + ).copyWith( color: Theme.of(context) .extension()! .textFieldActiveSearchIconRight, @@ -1801,7 +1836,7 @@ class _DesktopSendState extends ConsumerState { enabled: ref.watch(pPreviewTxButtonEnabled(coin)), onPressed: ref.watch(pPreviewTxButtonEnabled(coin)) ? previewSend : null, - ) + ), ], ); } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index 0887036a6..eef67e694 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -56,13 +56,13 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class DesktopTokenSend extends ConsumerStatefulWidget { const DesktopTokenSend({ - Key? key, + super.key, required this.walletId, this.autoFillData, this.clipboard = const ClipboardWrapper(), this.barcodeScanner = const BarcodeScannerWrapper(), this.accountLite, - }) : super(key: key); + }); final String walletId; final SendViewAutoFillData? autoFillData; @@ -108,10 +108,14 @@ class _DesktopTokenSendState extends ConsumerState { final Amount amount = _amountToSend!; final Amount availableBalance = ref - .read(pTokenBalance(( - walletId: walletId, - contractAddress: tokenWallet.tokenContract.address - ))) + .read( + pTokenBalance( + ( + walletId: walletId, + contractAddress: tokenWallet.tokenContract.address + ), + ), + ) .spendable; // confirm send all @@ -221,6 +225,7 @@ class _DesktopTokenSendState extends ConsumerState { padding: const EdgeInsets.all(32), child: BuildingTransactionDialog( coin: tokenWallet.cryptoCurrency.coin, + isSpark: false, onCancel: () { wasCancelled = true; @@ -250,7 +255,7 @@ class _DesktopTokenSendState extends ConsumerState { address: _address!, amount: amount, isChange: false, - ) + ), ], feeRateType: ref.read(feeRateTypeStateProvider), nonce: int.tryParse(nonceController.text), @@ -405,8 +410,10 @@ class _DesktopTokenSendState extends ConsumerState { _cachedAmountToSend == _amountToSend) { return; } - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info, + ); _cachedAmountToSend = _amountToSend; final price = ref @@ -468,8 +475,10 @@ class _DesktopTokenSendState extends ConsumerState { final qrResult = await scanner.scan(); - Logging.instance.log("qrResult content: ${qrResult.rawContent}", - level: LogLevel.Info); + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info, + ); final results = AddressUtils.parseUri(qrResult.rawContent); @@ -524,8 +533,9 @@ class _DesktopTokenSendState extends ConsumerState { // here we ignore the exception caused by not giving permission // to use the camera to scan a qr code Logging.instance.log( - "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", - level: LogLevel.Warning); + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); } } @@ -579,8 +589,10 @@ class _DesktopTokenSendState extends ConsumerState { return; } _cachedAmountToSend = _amountToSend; - Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", - level: LogLevel.Info); + Logging.instance.log( + "it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info, + ); final amountString = ref.read(pAmountFormatter(coin)).format( _amountToSend!, @@ -603,10 +615,15 @@ class _DesktopTokenSendState extends ConsumerState { Future sendAllTapped() async { cryptoAmountController.text = ref - .read(pTokenBalance(( - walletId: walletId, - contractAddress: ref.read(pCurrentTokenWallet)!.tokenContract.address - ))) + .read( + pTokenBalance( + ( + walletId: walletId, + contractAddress: + ref.read(pCurrentTokenWallet)!.tokenContract.address + ), + ), + ) .spendable .decimal .toStringAsFixed( @@ -788,9 +805,10 @@ class _DesktopTokenSendState extends ConsumerState { child: Text( tokenContract.symbol, style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -850,12 +868,15 @@ class _DesktopTokenSendState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(12), child: Text( - ref.watch(prefsChangeNotifierProvider - .select((value) => value.currency)), + ref.watch( + prefsChangeNotifierProvider + .select((value) => value.currency), + ), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), + color: Theme.of(context) + .extension()! + .accentColorDark, + ), ), ), ), @@ -936,12 +957,15 @@ class _DesktopTokenSendState extends ConsumerState { _addressToggleFlag ? TextFieldIconButton( key: const Key( - "sendTokenViewClearAddressFieldButtonKey"), + "sendTokenViewClearAddressFieldButtonKey", + ), onTap: () { sendToController.text = ""; _address = ""; _updatePreviewButtonState( - _address, _amountToSend); + _address, + _amountToSend, + ); setState(() { _addressToggleFlag = false; }); @@ -950,7 +974,8 @@ class _DesktopTokenSendState extends ConsumerState { ) : TextFieldIconButton( key: const Key( - "sendTokenViewPasteAddressFieldButtonKey"), + "sendTokenViewPasteAddressFieldButtonKey", + ), onTap: pasteAddress, child: sendToController.text.isEmpty ? const ClipboardIcon() @@ -1129,7 +1154,7 @@ class _DesktopTokenSendState extends ConsumerState { onPressed: ref.watch(previewTokenTxButtonStateProvider.state).state ? previewSend : null, - ) + ), ], ); } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index a330cb781..63974518b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -10,21 +10,27 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/widgets/custom_tab_view.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class MyWallet extends ConsumerStatefulWidget { const MyWallet({ - Key? key, + super.key, required this.walletId, this.contractAddress, - }) : super(key: key); + }); final String walletId; final String? contractAddress; @@ -40,11 +46,15 @@ class _MyWalletState extends ConsumerState { ]; late final bool isEth; + late final Coin coin; + late final bool isFrost; @override void initState() { - isEth = ref.read(pWallets).getWallet(widget.walletId).info.coin == - Coin.ethereum; + final wallet = ref.read(pWallets).getWallet(widget.walletId); + coin = wallet.info.coin; + isFrost = wallet is BitcoinFrostWallet; + isEth = coin == Coin.ethereum; if (isEth && widget.contractAddress == null) { titles.add("Transactions"); @@ -64,12 +74,60 @@ class _MyWalletState extends ConsumerState { titles: titles, children: [ widget.contractAddress == null - ? Padding( - padding: const EdgeInsets.all(20), - child: DesktopSend( - walletId: widget.walletId, - ), - ) + ? isFrost + ? Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: + const EdgeInsets.fromLTRB(0, 20, 0, 0), + child: SecondaryButton( + width: 200, + buttonHeight: ButtonHeight.l, + label: "Import sign config", + onPressed: () async { + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId) + as BitcoinFrostWallet; + ref.read(pFrostScaffoldArgs.state).state = + ( + info: ( + walletName: wallet.info.name, + frostCurrency: wallet.cryptoCurrency, + ), + walletId: widget.walletId, + stepRoutes: FrostRouteGenerator + .signFrostTxStepRoutes, + parentNav: Navigator.of(context), + frostInterruptionDialogType: + FrostInterruptionDialogType + .transactionCreation, + callerRouteName: MyStackView.routeName, + ); + + await Navigator.of(context).pushNamed( + FrostStepScaffold.routeName, + ); + }, + ), + ), + ], + ), + FrostSendView( + walletId: widget.walletId, + coin: coin, + ), + ], + ) + : Padding( + padding: const EdgeInsets.all(20), + child: DesktopSend( + walletId: widget.walletId, + ), + ) : Padding( padding: const EdgeInsets.all(20), child: DesktopTokenSend( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 0a9a5a29e..52fe50a4f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -21,6 +21,7 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; @@ -80,19 +81,31 @@ class _UnlockWalletKeysDesktopState Navigator.of(context, rootNavigator: true).pop(); final wallet = ref.read(pWallets).getWallet(widget.walletId); + ({String keys, String config})? frostData; + List? words; - // TODO: [prio=med] handle wallets that don't have a mnemonic + // TODO: [prio=low] handle wallets that don't have a mnemonic // All wallets currently are mnemonic based if (wallet is! MnemonicInterface) { - throw Exception("FIXME ~= see todo in code"); + if (wallet is BitcoinFrostWallet) { + frostData = ( + keys: (await wallet.getSerializedKeys())!, + config: (await wallet.getMultisigConfig())!, + ); + } else { + throw Exception("FIXME ~= see todo in code"); + } + } else { + words = await wallet.getMnemonicAsWords(); } - final words = await wallet.getMnemonicAsWords(); - if (mounted) { await Navigator.of(context).pushReplacementNamed( WalletKeysDesktopPopup.routeName, - arguments: words, + arguments: ( + mnemonic: words ?? [], + frostData: frostData, + ), ); } } else { @@ -301,21 +314,35 @@ class _UnlockWalletKeysDesktopState if (verified) { Navigator.of(context, rootNavigator: true).pop(); + ({String keys, String config})? frostData; + List? words; + final wallet = ref.read(pWallets).getWallet(widget.walletId); // TODO: [prio=low] handle wallets that don't have a mnemonic // All wallets currently are mnemonic based if (wallet is! MnemonicInterface) { - throw Exception("FIXME ~= see todo in code"); + if (wallet is BitcoinFrostWallet) { + frostData = ( + keys: (await wallet.getSerializedKeys())!, + config: (await wallet.getMultisigConfig())!, + ); + } else { + throw Exception("FIXME ~= see todo in code"); + } + } else { + words = await wallet.getMnemonicAsWords(); } - final words = await wallet.getMnemonicAsWords(); if (mounted) { await Navigator.of(context) .pushReplacementNamed( WalletKeysDesktopPopup.routeName, - arguments: words, + arguments: ( + mnemonic: words ?? [], + frostData: frostData, + ), ); } } else { diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart index 14574a083..606ae21f4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/address_utils.dart'; @@ -24,15 +25,18 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletKeysDesktopPopup extends StatelessWidget { const WalletKeysDesktopPopup({ Key? key, required this.words, + this.frostData, this.clipboardInterface = const ClipboardWrapper(), }) : super(key: key); final List words; + final ({String keys, String config})? frostData; final ClipboardInterface clipboardInterface; static const String routeName = "walletKeysDesktopPopup"; @@ -66,85 +70,185 @@ class WalletKeysDesktopPopup extends StatelessWidget { const SizedBox( height: 28, ), - Text( - "Recovery phrase", - style: STextStyles.desktopTextMedium(context), - ), - const SizedBox( - height: 8, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: Text( - "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", - style: STextStyles.desktopTextExtraExtraSmall(context), - textAlign: TextAlign.center, - ), - ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: MnemonicTable( - words: words, - isDesktop: true, - itemBorderColor: Theme.of(context) - .extension()! - .buttonBackSecondary, - ), - ), - const SizedBox( - height: 24, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Show QR code", - onPressed: () { - final String value = AddressUtils.encodeQRSeedData(words); - Navigator.of(context).pushNamed( - QRCodeDesktopPopupContent.routeName, - arguments: value, - ); - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( - label: "Copy", - onPressed: () async { - await clipboardInterface.setData( - ClipboardData(text: words.join(" ")), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, + frostData != null + ? Column( + children: [ + Text( + "Keys", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, ), - ); - }, - ), + child: RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 9), + child: Row( + children: [ + Flexible( + child: SelectableText( + frostData!.keys, + style: STextStyles.desktopTextExtraExtraSmall( + context), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + width: 10, + ), + IconCopyButton( + data: frostData!.keys, + ) + // TODO [prio=low: Add QR code button and dialog. + ], + ), + ), + ), + ), + const SizedBox( + height: 24, + ), + Text( + "Config", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: RoundedWhiteContainer( + borderColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 9), + child: Row( + children: [ + Flexible( + child: SelectableText( + frostData!.config, + style: STextStyles.desktopTextExtraExtraSmall( + context), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + width: 10, + ), + IconCopyButton( + data: frostData!.config, + ) + // TODO [prio=low: Add QR code button and dialog. + ], + ), + ), + ), + ), + const SizedBox( + height: 24, + ), + ], + ) + : Column( + children: [ + Text( + "Recovery phrase", + style: STextStyles.desktopTextMedium(context), + ), + const SizedBox( + height: 8, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Text( + "Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.", + style: + STextStyles.desktopTextExtraExtraSmall(context), + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: MnemonicTable( + words: words, + isDesktop: true, + itemBorderColor: Theme.of(context) + .extension()! + .buttonBackSecondary, + ), + ), + const SizedBox( + height: 24, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Show QR code", + onPressed: () { + // TODO: address utils + final String value = + AddressUtils.encodeQRSeedData(words); + Navigator.of(context).pushNamed( + QRCodeDesktopPopupContent.routeName, + arguments: value, + ); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Copy", + onPressed: () async { + await clipboardInterface.setData( + ClipboardData(text: words.join(" ")), + ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + } + }, + ), + ), + ], + ), + ), + ], ), - ], - ), - ), const SizedBox( height: 32, ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart index f495475db..c27b7855d 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart @@ -14,6 +14,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; @@ -34,7 +35,8 @@ enum _WalletOptions { changeRepresentative, showXpub, lelantusCoins, - sparkCoins; + sparkCoins, + frostOptions; String get prettyName { switch (this) { @@ -50,6 +52,8 @@ enum _WalletOptions { return "Lelantus Coins"; case _WalletOptions.sparkCoins: return "Spark Coins"; + case _WalletOptions.frostOptions: + return "FROST settings"; } } } @@ -96,6 +100,9 @@ class WalletOptionsButton extends StatelessWidget { onFiroShowSparkCoins: () async { Navigator.of(context).pop(_WalletOptions.sparkCoins); }, + onFrostMSWalletOptionsPressed: () async { + Navigator.of(context).pop(_WalletOptions.frostOptions); + }, walletId: walletId, ); }, @@ -207,6 +214,15 @@ class WalletOptionsButton extends StatelessWidget { ), ); break; + + case _WalletOptions.frostOptions: + unawaited( + Navigator.of(context).pushNamed( + FrostMSWalletOptionsView.routeName, + arguments: walletId, + ), + ); + break; } } }, @@ -241,6 +257,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { required this.onChangeRepPressed, required this.onFiroShowLelantusCoins, required this.onFiroShowSparkCoins, + required this.onFrostMSWalletOptionsPressed, required this.walletId, }) : super(key: key); @@ -250,6 +267,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget { final VoidCallback onChangeRepPressed; final VoidCallback onFiroShowLelantusCoins; final VoidCallback onFiroShowSparkCoins; + final VoidCallback onFrostMSWalletOptionsPressed; final String walletId; @override @@ -265,6 +283,9 @@ class WalletOptionsPopupMenu extends ConsumerWidget { final bool canChangeRep = coin == Coin.nano || coin == Coin.banano; + final bool isFrost = + coin == Coin.bitcoinFrost || coin == Coin.bitcoinFrostTestNet; + return Stack( children: [ Positioned( @@ -429,6 +450,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget { ), ), ), + if (isFrost) + const SizedBox( + height: 8, + ), + if (isFrost) + TransparentButton( + onPressed: onFrostMSWalletOptionsPressed, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.addressBookDesktop, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + const SizedBox(width: 14), + Expanded( + child: Text( + _WalletOptions.frostOptions.prettyName, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ], + ), + ), + ), if (xpubEnabled) const SizedBox( height: 8, diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index c28524635..b258d0a5e 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -234,7 +234,7 @@ class _DesktopOrdinalDetailsViewState final path = await showLoading( whileFuture: _savePngToFile(), context: context, - isDesktop: true, + rootNavigator: true, message: "Saving ordinal image", onException: (e) { didError = true; diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index 1fb4f29af..c710a4d10 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -210,7 +210,7 @@ class _DesktopOrdinals extends ConsumerState { onPressed: () async { // show loading for a minimum of 2 seconds on refreshing await showLoading( - isDesktop: true, + rootNavigator: true, whileFuture: Future.wait([ Future.delayed(const Duration(seconds: 2)), (ref.read(pWallets).getWallet(widget.walletId) diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart index 677366c09..2e27798bb 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart @@ -99,7 +99,7 @@ class _CreateAutoBackup extends ConsumerState { passphraseFocusNode = FocusNode(); passphraseRepeatFocusNode = FocusNode(); - if (Platform.isAndroid) { + if (Platform.isAndroid || Platform.isIOS) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final dir = await stackFileSystem.prepareStorage(); if (mounted) { @@ -174,14 +174,14 @@ class _CreateAutoBackup extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) Consumer(builder: (context, ref, __) { return Container( color: Colors.transparent, child: TextField( autocorrect: false, enableSuggestions: false, - onTap: Platform.isAndroid + onTap: Platform.isAndroid || Platform.isIOS ? null : () async { try { @@ -241,7 +241,7 @@ class _CreateAutoBackup extends ConsumerState { ), ); }), - if (!Platform.isAndroid) + if (!Platform.isAndroid && !Platform.isIOS) const SizedBox( height: 24, ), diff --git a/lib/providers/frost_wallet/frost_wallet_providers.dart b/lib/providers/frost_wallet/frost_wallet_providers.dart new file mode 100644 index 000000000..7b3ee3eda --- /dev/null +++ b/lib/providers/frost_wallet/frost_wallet_providers.dart @@ -0,0 +1,104 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frostdart/frostdart_bindings_generated.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; + +// =================== wallet creation ========================================= +final pFrostMultisigConfig = StateProvider((ref) => null); +final pFrostMyName = StateProvider((ref) => null); + +final pFrostStartKeyGenData = StateProvider< + ({ + String seed, + String commitments, + Pointer multisigConfigWithNamePtr, + Pointer secretShareMachineWrapperPtr, + })?>((_) => null); + +final pFrostSecretSharesData = StateProvider< + ({ + String share, + Pointer secretSharesResPtr, + })?>((ref) => null); + +final pFrostCompletedKeyGenData = StateProvider< + ({ + Uint8List multisigId, + String recoveryString, + String serializedKeys, + })?>((ref) => null); + +// ================= transaction creation ====================================== +final pFrostTxData = StateProvider((ref) => null); + +final pFrostAttemptSignData = StateProvider< + ({ + Pointer machinePtr, + String preprocess, + })?>((ref) => null); + +final pFrostContinueSignData = StateProvider< + ({ + Pointer machinePtr, + String share, + })?>((ref) => null); + +// ===================== shared/util =========================================== +final pFrostSelectParticipantsUnordered = + StateProvider?>((ref) => null); + +// ========================= resharing ========================================= +final pFrostResharingData = Provider((ref) => _ResharingData()); + +class _ResharingData { + String? myName; + + IncompleteFrostWallet? incompleteWallet; + + // resharer encoded config string + String? resharerRConfig; + + ({ + int newThreshold, + Map resharers, + List newParticipants, + })? get configData => resharerRConfig != null + ? Frost.extractResharerConfigData(rConfig: resharerRConfig!) + : null; + + // resharer start string (for sharing) and machine + ({ + String resharerStart, + Pointer machine, + })? startResharerData; + + // reshared start string (for sharing) and machine + ({ + String resharedStart, + Pointer prior, + })? startResharedData; + + // resharer complete string (for sharing) + String? resharerComplete; + + // new keys and config with an ID + ({ + String multisigConfig, + String serializedKeys, + String resharedId, + })? newWalletData; + + // reset/clear all data + void reset() { + resharerRConfig = null; + startResharerData = null; + startResharedData = null; + resharerComplete = null; + newWalletData = null; + incompleteWallet = null; + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index a046cc01d..f6dcaa6da 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -26,6 +26,9 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_tok import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/select_new_frost_import_type_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; @@ -76,6 +79,7 @@ import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.d import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; +import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/send_view/token_send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; @@ -113,6 +117,10 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_pr import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; @@ -181,9 +189,11 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/widgets/choose_coin_view.dart'; +import 'package:stackwallet/widgets/frost_scaffold.dart'; import 'package:tuple/tuple.dart'; /* @@ -423,6 +433,144 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case CreateNewFrostMsWalletView.routeName: + if (args is ({ + String walletName, + FrostCurrency frostCurrency, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => CreateNewFrostMsWalletView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case RestoreFrostMsWalletView.routeName: + if (args is ({ + String walletName, + FrostCurrency frostCurrency, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => RestoreFrostMsWalletView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case SelectNewFrostImportTypeView.routeName: + if (args is ({ + String walletName, + FrostCurrency frostCurrency, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SelectNewFrostImportTypeView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case FrostStepScaffold.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const FrostStepScaffold(), + settings: RouteSettings( + name: settings.name, + ), + ); + + case FrostMSWalletOptionsView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostMSWalletOptionsView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case FrostParticipantsView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostParticipantsView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case InitiateResharingView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => InitiateResharingView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case CompleteReshareConfigView.routeName: + if (args is ({String walletId, Map resharers})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => CompleteReshareConfigView( + walletId: args.walletId, + resharers: args.resharers, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case FrostSendView.routeName: + if (args is ({ + String walletId, + Coin coin, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => FrostSendView( + walletId: args.walletId, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // case MonkeyLoadedView.routeName: // if (args is Tuple2>) { // return getRoute( @@ -1051,12 +1199,33 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case WalletBackupView.routeName: - if (args is Tuple2>) { + if (args is ({String walletId, List mnemonic})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => WalletBackupView( - walletId: args.item1, - mnemonic: args.item2, + walletId: args.walletId, + mnemonic: args.mnemonic, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } else if (args is ({ + String walletId, + List mnemonic, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? frostWalletData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + frostWalletData: args.frostWalletData, ), settings: RouteSettings( name: settings.name, @@ -1443,7 +1612,19 @@ class RouteGenerator { name: settings.name, ), ); + } else if (args is ({Coin coin, String walletId})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SendView( + walletId: args.walletId, + coin: args.coin, + ), + settings: RouteSettings( + name: settings.name, + ), + ); } + return _routeError("${settings.name} invalid args: ${args.toString()}"); case TokenSendView.routeName: @@ -1961,10 +2142,14 @@ class RouteGenerator { settings: RouteSettings(name: settings.name)); case WalletKeysDesktopPopup.routeName: - if (args is List) { + if (args is ({ + List mnemonic, + ({String keys, String config})? frostData + })) { return FadePageRoute( WalletKeysDesktopPopup( - words: args, + words: args.mnemonic, + frostData: args.frostData, ), RouteSettings( name: settings.name, diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 3931b4573..b9ac2224b 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -686,7 +686,7 @@ abstract class EthereumAPI { try { final response = await client.get( url: Uri.parse( - "$stackBaseServer/abis?addrs=$contractAddress&verbose=true", + "$stackBaseServer/abis?addrs=$contractAddress", ), proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.getProxyInfo() diff --git a/lib/services/frost.dart b/lib/services/frost.dart new file mode 100644 index 000000000..adf88695a --- /dev/null +++ b/lib/services/frost.dart @@ -0,0 +1,650 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:frostdart/frostdart.dart'; +import 'package:frostdart/frostdart_bindings_generated.dart'; +import 'package:frostdart/output.dart'; +import 'package:frostdart/util.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +abstract class Frost { + //==================== utility =============================================== + static List getParticipants({ + required String multisigConfig, + }) { + try { + final numberOfParticipants = multisigParticipants( + multisigConfig: multisigConfig, + ); + + final List participants = []; + for (int i = 0; i < numberOfParticipants; i++) { + participants.add( + multisigParticipant( + multisigConfig: multisigConfig, + index: i, + ), + ); + } + + return participants; + } catch (e, s) { + Logging.instance.log( + "getParticipants failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static bool validateEncodedMultisigConfig({required String encodedConfig}) { + try { + decodeMultisigConfig(multisigConfig: encodedConfig); + return true; + } catch (e, s) { + Logging.instance.log( + "validateEncodedMultisigConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + return false; + } + } + + static int getThreshold({ + required String multisigConfig, + }) { + try { + final threshold = multisigThreshold( + multisigConfig: multisigConfig, + ); + + return threshold; + } catch (e, s) { + Logging.instance.log( + "getThreshold failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + List<({String address, Amount amount})> recipients, + String changeAddress, + int feePerWeight, + List inputs, + }) extractDataFromSignConfig({ + required String signConfig, + required CryptoCurrency coin, + }) { + try { + final network = coin.network == CryptoCurrencyNetwork.test + ? Network.Testnet + : Network.Mainnet; + final signConfigPointer = decodedSignConfig( + encodedConfig: signConfig, + network: network, + ); + + // get various data from config + final feePerWeight = + signFeePerWeight(signConfigPointer: signConfigPointer); + final changeAddress = signChange(signConfigPointer: signConfigPointer); + final recipientsCount = signPayments( + signConfigPointer: signConfigPointer, + ); + + // get tx recipient info + final List<({String address, Amount amount})> recipients = []; + for (int i = 0; i < recipientsCount; i++) { + final String address = signPaymentAddress( + signConfigPointer: signConfigPointer, + index: i, + ); + final int amount = signPaymentAmount( + signConfigPointer: signConfigPointer, + index: i, + ); + recipients.add( + ( + address: address, + amount: Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.fractionDigits, + ), + ), + ); + } + + // get utxos + final count = signInputs(signConfigPointer: signConfigPointer); + final List outputs = []; + for (int i = 0; i < count; i++) { + final output = signInput( + signConfig: signConfig, + index: i, + network: network, + ); + + outputs.add(output); + } + + return ( + recipients: recipients, + changeAddress: changeAddress, + feePerWeight: feePerWeight, + inputs: outputs, + ); + } catch (e, s) { + Logging.instance.log( + "extractDataFromSignConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + //==================== wallet creation ======================================= + + static String createMultisigConfig({ + required String name, + required int threshold, + required List participants, + }) { + try { + final config = newMultisigConfig( + name: name, + threshold: threshold, + participants: participants, + ); + + return config; + } catch (e, s) { + Logging.instance.log( + "createMultisigConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + String seed, + String commitments, + Pointer multisigConfigWithNamePtr, + Pointer secretShareMachineWrapperPtr, + }) startKeyGeneration({ + required String multisigConfig, + required String myName, + }) { + try { + final startKeyGenResPtr = startKeyGen( + multisigConfig: multisigConfig, + myName: myName, + language: Language.english, + ); + + final seed = startKeyGenResPtr.ref.seed.toDartString(); + final commitments = startKeyGenResPtr.ref.commitments.toDartString(); + final configWithNamePtr = startKeyGenResPtr.ref.config; + final machinePtr = startKeyGenResPtr.ref.machine; + + return ( + seed: seed, + commitments: commitments, + multisigConfigWithNamePtr: configWithNamePtr, + secretShareMachineWrapperPtr: machinePtr, + ); + } catch (e, s) { + Logging.instance.log( + "startKeyGeneration failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + String share, + Pointer secretSharesResPtr, + }) generateSecretShares({ + required Pointer multisigConfigWithNamePtr, + required String mySeed, + required Pointer secretShareMachineWrapperPtr, + required List commitments, + }) { + try { + final secretSharesResPtr = getSecretShares( + multisigConfigWithName: multisigConfigWithNamePtr, + seed: mySeed, + language: Language.english, + machine: secretShareMachineWrapperPtr, + commitments: commitments, + ); + + final share = secretSharesResPtr.ref.shares.toDartString(); + + return (share: share, secretSharesResPtr: secretSharesResPtr); + } catch (e, s) { + Logging.instance.log( + "generateSecretShares failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + Uint8List multisigId, + String recoveryString, + String serializedKeys, + }) completeKeyGeneration({ + required Pointer multisigConfigWithNamePtr, + required Pointer secretSharesResPtr, + required List shares, + }) { + try { + final keyGenResPtr = completeKeyGen( + multisigConfigWithName: multisigConfigWithNamePtr, + machineAndCommitments: secretSharesResPtr, + shares: shares, + ); + + final id = Uint8List.fromList( + List.generate( + MULTISIG_ID_LENGTH, + (index) => keyGenResPtr.ref.multisig_id[index], + ), + ); + + final recoveryString = keyGenResPtr.ref.recovery.toDartString(); + + final serializedKeys = serializeKeys(keys: keyGenResPtr.ref.keys); + + return ( + multisigId: id, + recoveryString: recoveryString, + serializedKeys: serializedKeys, + ); + } catch (e, s) { + Logging.instance.log( + "completeKeyGeneration failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + //=================== transaction creation =================================== + + static String createSignConfig({ + required int network, + required List<({UTXO utxo, Uint8List scriptPubKey})> inputs, + required List<({String address, Amount amount, bool isChange})> outputs, + required String changeAddress, + required int feePerWeight, + }) { + try { + final signConfig = newSignConfig( + network: network, + outputs: inputs + .map( + (e) => Output( + hash: e.utxo.txid.toUint8ListFromHex, + vout: e.utxo.vout, + value: e.utxo.value, + scriptPubKey: e.scriptPubKey, + ), + ) + .toList(), + paymentAddresses: outputs.map((e) => e.address).toList(), + paymentAmounts: outputs.map((e) => e.amount.raw.toInt()).toList(), + change: changeAddress, + feePerWeight: feePerWeight, + ); + + return signConfig; + } catch (e, s) { + Logging.instance.log( + "createSignConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + Pointer machinePtr, + String preprocess, + }) attemptSignConfig({ + required int network, + required String config, + required String serializedKeys, + }) { + try { + final keys = deserializeKeys(keys: serializedKeys); + + final attemptSignRes = attemptSign( + thresholdKeysWrapperPointer: keys, + network: network, + signConfig: config, + ); + + return ( + preprocess: attemptSignRes.ref.preprocess.toDartString(), + machinePtr: attemptSignRes.ref.machine, + ); + } catch (e, s) { + Logging.instance.log( + "attemptSignConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + Pointer machinePtr, + String share, + }) continueSigning({ + required Pointer machinePtr, + required List preprocesses, + }) { + try { + final continueSignRes = continueSign( + machine: machinePtr, + preprocesses: preprocesses, + ); + + return ( + share: continueSignRes.ref.preprocess.toDartString(), + machinePtr: continueSignRes.ref.machine, + ); + } catch (e, s) { + Logging.instance.log( + "continueSigning failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static String completeSigning({ + required Pointer machinePtr, + required List shares, + }) { + try { + final rawTransaction = completeSign( + machine: machinePtr, + shares: shares, + ); + + return rawTransaction; + } catch (e, s) { + Logging.instance.log( + "completeSigning failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static Pointer decodedSignConfig({ + required String encodedConfig, + required int network, + }) { + try { + final configPtr = + decodeSignConfig(encodedSignConfig: encodedConfig, network: network); + return configPtr; + } catch (e, s) { + Logging.instance.log( + "decodedSignConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + //========================== resharing ======================================= + + static String createResharerConfig({ + required int newThreshold, + required List resharers, + required List newParticipants, + }) { + try { + final config = newResharerConfig( + newThreshold: newThreshold, + newParticipants: newParticipants, + resharers: resharers, + ); + + return config; + } catch (e, s) { + Logging.instance.log( + "createResharerConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + String resharerStart, + Pointer machine, + }) beginResharer({ + required String serializedKeys, + required String config, + }) { + try { + final result = startResharer( + serializedKeys: serializedKeys, + config: config, + ); + + return ( + resharerStart: result.encoded, + machine: result.machine, + ); + } catch (e, s) { + Logging.instance.log( + "beginResharer failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + /// expects [resharerStarts] of length equal to resharers. + static ({ + String resharedStart, + Pointer prior, + }) beginReshared({ + required String myName, + required String resharerConfig, + required List resharerStarts, + }) { + try { + final result = startReshared( + newMultisigName: 'unused_property', + myName: myName, + resharerConfig: resharerConfig, + resharerStarts: resharerStarts, + ); + return ( + resharedStart: result.encoded, + prior: result.machine, + ); + } catch (e, s) { + Logging.instance.log( + "beginReshared failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + /// expects [encryptionKeysOfResharedTo] of length equal to new participants + static String finishResharer({ + required StartResharerRes machine, + required List encryptionKeysOfResharedTo, + }) { + try { + final result = completeResharer( + machine: machine, + encryptionKeysOfResharedTo: encryptionKeysOfResharedTo, + ); + return result; + } catch (e, s) { + Logging.instance.log( + "finishResharer failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + /// expects [resharerCompletes] of length equal to resharers + static ({ + String multisigConfig, + String serializedKeys, + String resharedId, + }) finishReshared({ + required StartResharedRes prior, + required List resharerCompletes, + }) { + try { + final result = completeReshared( + prior: prior, + resharerCompletes: resharerCompletes, + ); + return result; + } catch (e, s) { + Logging.instance.log( + "finishReshared failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static Pointer decodedResharerConfig({ + required String resharerConfig, + }) { + try { + final config = decodeResharerConfig(resharerConfig: resharerConfig); + + return config; + } catch (e, s) { + Logging.instance.log( + "decodedResharerConfig failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static ({ + int newThreshold, + Map resharers, + List newParticipants, + }) extractResharerConfigData({ + required String rConfig, + }) { + final decoded = _decodeRConfigWithResharers(rConfig); + final resharerConfig = decoded.config; + + try { + final newThreshold = resharerNewThreshold( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + ); + + final resharersCount = resharerResharers( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + ); + final List resharers = []; + for (int i = 0; i < resharersCount; i++) { + resharers.add( + resharerResharer( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + index: i, + ), + ); + } + + final newParticipantsCount = resharerNewParticipants( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + ); + final List newParticipants = []; + for (int i = 0; i < newParticipantsCount; i++) { + newParticipants.add( + resharerNewParticipant( + resharerConfigPointer: decodedResharerConfig( + resharerConfig: resharerConfig, + ), + index: i, + ), + ); + } + + final Map resharersMap = {}; + + for (final resharer in resharers) { + resharersMap[decoded.resharers.entries + .firstWhere((e) => e.value == resharer) + .key] = resharer; + } + + return ( + newThreshold: newThreshold, + resharers: resharersMap, + newParticipants: newParticipants, + ); + } catch (e, s) { + Logging.instance.log( + "extractResharerConfigData failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + static String encodeRConfig( + String config, + Map resharers, + ) { + return base64Encode("$config@${jsonEncode(resharers)}".toUint8ListFromUtf8); + } + + static String decodeRConfig( + String rConfig, + ) { + return base64Decode(rConfig).toUtf8String.split("@").first; + } + + static ({Map resharers, String config}) + _decodeRConfigWithResharers( + String rConfig, + ) { + final parts = base64Decode(rConfig).toUtf8String.split("@"); + + final config = parts[0]; + final resharers = Map.from(jsonDecode(parts[1]) as Map); + + return (resharers: resharers, config: config); + } +} diff --git a/lib/services/notifications_api.dart b/lib/services/notifications_api.dart index 3ef97462a..010e3c220 100644 --- a/lib/services/notifications_api.dart +++ b/lib/services/notifications_api.dart @@ -1,4 +1,4 @@ -/* +/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack @@ -9,14 +9,13 @@ */ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:stackwallet/models/notification_model.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/utilities/prefs.dart'; class NotificationApi { static final _notifications = FlutterLocalNotificationsPlugin(); - static final onNotifications = BehaviorSubject(); + // static final onNotifications = BehaviorSubject(); static Future _notificationDetails() async { return const NotificationDetails( @@ -25,17 +24,17 @@ class NotificationApi { // importance: Importance.max, priority: Priority.high, ticker: 'ticker'), - iOS: IOSNotificationDetails(), - macOS: MacOSNotificationDetails(), + iOS: DarwinNotificationDetails(), + macOS: DarwinNotificationDetails(), ); } static Future init({bool initScheduled = false}) async { const android = AndroidInitializationSettings('app_icon_alpha'); - const iOS = IOSInitializationSettings(); + const iOS = DarwinInitializationSettings(); const linux = LinuxInitializationSettings( defaultActionName: "temporary_stack_wallet"); - const macOS = MacOSInitializationSettings(); + const macOS = DarwinInitializationSettings(); const settings = InitializationSettings( android: android, iOS: iOS, @@ -44,9 +43,12 @@ class NotificationApi { ); await _notifications.initialize( settings, - onSelectNotification: (payload) async { - onNotifications.add(payload); - }, + // onDidReceiveNotificationResponse: (payload) async { + // onNotifications.add(payload.payload); + // }, + // onDidReceiveBackgroundNotificationResponse: (payload) async { + // onNotifications.add(payload.payload); + // }, ); } diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index e6c49f47f..182de8747 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -24,6 +24,7 @@ import 'package:stackwallet/services/wallets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import 'exchange/exchange.dart'; @@ -123,7 +124,7 @@ class NotificationsService extends ChangeNotifier { final node = nodeService.getPrimaryNodeFor(coin: coin); if (node != null) { - if (coin.isElectrumXCoin) { + if (wallet is ElectrumXInterface) { final eNode = ElectrumXNode( address: node.host, port: node.port, @@ -146,7 +147,7 @@ class NotificationsService extends ChangeNotifier { node: eNode, failovers: failovers, prefs: prefs, - coin: coin, + cryptoCurrency: wallet.cryptoCurrency, ); final tx = await client.getTransaction(txHash: txid); diff --git a/lib/services/price.dart b/lib/services/price.dart index ce1a57301..281372dcd 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -101,7 +101,7 @@ class PriceAPI { "https://api.coingecko.com/api/v3/coins/markets?vs_currency" "=${baseCurrency.toLowerCase()}" "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," - "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos" + "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos,solana" "&order=market_cap_desc&per_page=50&page=1&sparkline=false"); final coinGeckoResponse = await client.get( @@ -187,34 +187,39 @@ class PriceAPI { } try { - final contractAddressesString = - contractAddresses.reduce((value, element) => "$value,$element"); - final uri = Uri.parse( - "https://api.coingecko.com/api/v3/simple/token_price/ethereum" - "?vs_currencies=${baseCurrency.toLowerCase()}&contract_addresses" - "=$contractAddressesString&include_24hr_change=true"); - - final coinGeckoResponse = await client.get( - url: uri, - headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map; - - for (final key in coinGeckoData.keys) { - final contractAddress = key as String; - - final map = coinGeckoData[contractAddress] as Map; - - final price = Decimal.parse(map[baseCurrency.toLowerCase()].toString()); - final change24h = double.parse( - map["${baseCurrency.toLowerCase()}_24h_change"].toString()); - - tokenPrices[contractAddress] = Tuple2(price, change24h); - } + // for (final contractAddress in contractAddresses) { + // final uri = Uri.parse( + // "https://api.coingecko.com/api/v3/simple/token_price/ethereum" + // "?vs_currencies=${baseCurrency.toLowerCase()}&contract_addresses" + // "=$contractAddress&include_24hr_change=true"); + // + // final coinGeckoResponse = await client.get( + // url: uri, + // headers: {'Content-Type': 'application/json'}, + // proxyInfo: Prefs.instance.useTor + // ? TorService.sharedInstance.getProxyInfo() + // : null, + // ); + // + // try { + // final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map; + // + // final map = coinGeckoData[contractAddress] as Map; + // + // final price = + // Decimal.parse(map[baseCurrency.toLowerCase()].toString()); + // final change24h = double.parse( + // map["${baseCurrency.toLowerCase()}_24h_change"].toString()); + // + // tokenPrices[contractAddress] = Tuple2(price, change24h); + // } catch (e, s) { + // // only log the error as we don't want to interrupt the rest of the loop + // Logging.instance.log( + // "getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddress): $e\n$s\nRESPONSE: ${coinGeckoResponse.body}", + // level: LogLevel.Warning, + // ); + // } + // } return tokenPrices; } catch (e, s) { diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 729b863ee..bba7bfe90 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:isar/isar.dart'; @@ -16,7 +18,6 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; @@ -134,6 +135,12 @@ class Wallets { } Future load(Prefs prefs, MainDB mainDB) async { + // return await _loadV1(prefs, mainDB); + // return await _loadV2(prefs, mainDB); + return await _loadV3(prefs, mainDB); + } + + Future _loadV1(Prefs prefs, MainDB mainDB) async { if (hasLoaded) { return; } @@ -156,10 +163,11 @@ class Wallets { return; } - List> walletInitFutures = []; - List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = []; + final List> walletInitFutures = []; + final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = + []; - List walletIdsToEnableAutoSync = []; + final List walletIdsToEnableAutoSync = []; bool shouldAutoSyncAll = false; switch (prefs.syncType) { case SyncingType.currentWalletOnly: @@ -185,8 +193,8 @@ class Wallets { if (isVerified) { // TODO: integrate this into the new wallets somehow? // requires some thinking - final txTracker = - TransactionNotificationTracker(walletId: walletInfo.walletId); + // final txTracker = + // TransactionNotificationTracker(walletId: walletInfo.walletId); final wallet = await Wallet.load( walletId: walletInfo.walletId, @@ -233,15 +241,289 @@ class Wallets { } } + /// should be fastest but big ui performance hit + Future _loadV2(Prefs prefs, MainDB mainDB) async { + if (hasLoaded) { + return; + } + hasLoaded = true; + + // clear out any wallet hive boxes where the wallet was deleted in previous app run + for (final walletId in DB.instance + .values(boxName: DB.boxNameWalletsToDeleteOnStart)) { + await mainDB.isar.writeTxn(() async => await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .deleteAll()); + } + // clear list + await DB.instance + .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); + + final walletInfoList = await mainDB.isar.walletInfo.where().findAll(); + if (walletInfoList.isEmpty) { + return; + } + + final List> walletIDInitFutures = []; + final List> deleteFutures = []; + final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = + []; + + final List walletIdsToEnableAutoSync = []; + bool shouldAutoSyncAll = false; + switch (prefs.syncType) { + case SyncingType.currentWalletOnly: + // do nothing as this will be set when going into a wallet from the main screen + break; + case SyncingType.selectedWalletsAtStartup: + walletIdsToEnableAutoSync.addAll(prefs.walletIdsSyncOnStartup); + break; + case SyncingType.allWalletsOnStartup: + shouldAutoSyncAll = true; + break; + } + + for (final walletInfo in walletInfoList) { + try { + final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); + Logging.instance.log( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + level: LogLevel.Info, + ); + + if (isVerified) { + // TODO: integrate this into the new wallets somehow? + // requires some thinking + // final txTracker = + // TransactionNotificationTracker(walletId: walletInfo.walletId); + + final walletIdCompleter = Completer(); + + walletIDInitFutures.add(walletIdCompleter.future); + + await Wallet.load( + walletId: walletInfo.walletId, + mainDB: mainDB, + secureStorageInterface: nodeService.secureStorageInterface, + nodeService: nodeService, + prefs: prefs, + ).then((wallet) { + if (wallet is CwBasedInterface) { + // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); + + walletIdCompleter.complete("dummy_ignore"); + } else { + walletIdCompleter.complete(wallet.walletId); + } + + _wallets[wallet.walletId] = wallet; + }); + } else { + // wallet creation was not completed by user so we remove it completely + deleteFutures.add(_deleteWallet(walletInfo.walletId)); + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Fatal); + continue; + } + } + + final asyncWalletIds = await Future.wait(walletIDInitFutures); + asyncWalletIds.removeWhere((e) => e == "dummy_ignore"); + + final List> walletInitFutures = asyncWalletIds + .map( + (id) => _wallets[id]!.init().then( + (_) { + if (shouldAutoSyncAll || walletIdsToEnableAutoSync.contains(id)) { + _wallets[id]!.shouldAutoSync = true; + } + }, + ), + ) + .toList(); + + if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { + unawaited(Future.wait([ + _initLinearly(walletsToInitLinearly), + ...walletInitFutures, + ])); + } else if (walletInitFutures.isNotEmpty) { + unawaited(Future.wait(walletInitFutures)); + } else if (walletsToInitLinearly.isNotEmpty) { + unawaited(_initLinearly(walletsToInitLinearly)); + } + + // finally await any deletions that haven't completed yet + await Future.wait(deleteFutures); + } + + /// should be best performance + Future _loadV3(Prefs prefs, MainDB mainDB) async { + if (hasLoaded) { + return; + } + hasLoaded = true; + + // clear out any wallet hive boxes where the wallet was deleted in previous app run + for (final walletId in DB.instance + .values(boxName: DB.boxNameWalletsToDeleteOnStart)) { + await mainDB.isar.writeTxn(() async => await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .deleteAll()); + } + // clear list + await DB.instance + .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); + + final walletInfoList = await mainDB.isar.walletInfo.where().findAll(); + if (walletInfoList.isEmpty) { + return; + } + + final List> walletIDInitFutures = []; + final List> deleteFutures = []; + final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = + []; + + final List walletIdsToSyncOnceOnStartup = []; + bool shouldSyncAllOnceOnStartup = false; + switch (prefs.syncType) { + case SyncingType.currentWalletOnly: + // do nothing as this will be set when going into a wallet from the main screen + break; + case SyncingType.selectedWalletsAtStartup: + walletIdsToSyncOnceOnStartup.addAll(prefs.walletIdsSyncOnStartup); + break; + case SyncingType.allWalletsOnStartup: + shouldSyncAllOnceOnStartup = true; + break; + } + + for (final walletInfo in walletInfoList) { + try { + final isVerified = await walletInfo.isMnemonicVerified(mainDB.isar); + Logging.instance.log( + "LOADING WALLET: ${walletInfo.name}:${walletInfo.walletId} " + "IS VERIFIED: $isVerified", + level: LogLevel.Info, + ); + + if (isVerified) { + // TODO: integrate this into the new wallets somehow? + // requires some thinking + // final txTracker = + // TransactionNotificationTracker(walletId: walletInfo.walletId); + + final walletIdCompleter = Completer(); + + walletIDInitFutures.add(walletIdCompleter.future); + + await Wallet.load( + walletId: walletInfo.walletId, + mainDB: mainDB, + secureStorageInterface: nodeService.secureStorageInterface, + nodeService: nodeService, + prefs: prefs, + ).then((wallet) { + if (wallet is CwBasedInterface) { + // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); + + walletIdCompleter.complete("dummy_ignore"); + } else { + walletIdCompleter.complete(wallet.walletId); + } + + _wallets[wallet.walletId] = wallet; + }); + } else { + // wallet creation was not completed by user so we remove it completely + deleteFutures.add(_deleteWallet(walletInfo.walletId)); + } + } catch (e, s) { + Logging.instance.log("$e $s", level: LogLevel.Fatal); + continue; + } + } + + final asyncWalletIds = await Future.wait(walletIDInitFutures); + asyncWalletIds.removeWhere((e) => e == "dummy_ignore"); + + final List idsToRefresh = []; + final List> walletInitFutures = asyncWalletIds + .map( + (id) => _wallets[id]!.init().then( + (_) { + if (shouldSyncAllOnceOnStartup || + walletIdsToSyncOnceOnStartup.contains(id)) { + idsToRefresh.add(id); + } + }, + ), + ) + .toList(); + + Future _refreshFutures(List idsToRefresh) async { + final start = DateTime.now(); + Logging.instance.log( + "Initial refresh start: ${start.toUtc()}", + level: LogLevel.Warning, + ); + const groupCount = 3; + for (int i = 0; i < idsToRefresh.length; i += groupCount) { + final List> futures = []; + for (int j = 0; j < groupCount; j++) { + if (i + j >= idsToRefresh.length) { + break; + } + futures.add( + _wallets[idsToRefresh[i + j]]!.refresh(), + ); + } + await Future.wait(futures); + } + Logging.instance.log( + "Initial refresh duration: ${DateTime.now().difference(start)}", + level: LogLevel.Warning, + ); + } + + if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { + unawaited( + Future.wait([ + _initLinearly(walletsToInitLinearly), + ...walletInitFutures, + ]).then( + (value) => _refreshFutures(idsToRefresh), + ), + ); + } else if (walletInitFutures.isNotEmpty) { + unawaited( + Future.wait(walletInitFutures).then( + (value) => _refreshFutures(idsToRefresh), + ), + ); + } else if (walletsToInitLinearly.isNotEmpty) { + unawaited(_initLinearly(walletsToInitLinearly)); + } + + // finally await any deletions that haven't completed yet + await Future.wait(deleteFutures); + } + Future loadAfterStackRestore( Prefs prefs, List wallets, bool isDesktop, ) async { - List> walletInitFutures = []; - List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = []; + final List> walletInitFutures = []; + final List<({Wallet wallet, bool shouldAutoSync})> walletsToInitLinearly = + []; - List walletIdsToEnableAutoSync = []; + final List walletIdsToEnableAutoSync = []; bool shouldAutoSyncAll = false; switch (prefs.syncType) { case SyncingType.currentWalletOnly: @@ -270,10 +552,10 @@ class Wallets { if (wallet is CwBasedInterface) { // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { - walletInitFutures.add(wallet.init().then((value) { - if (shouldSetAutoSync) { - wallet.shouldAutoSync = true; - } + walletInitFutures.add(wallet.init().then((_) { + // if (shouldSetAutoSync) { + // wallet.shouldAutoSync = true; + // } })); } } diff --git a/lib/supported_coins.dart b/lib/supported_coins.dart new file mode 100644 index 000000000..70ee44ad8 --- /dev/null +++ b/lib/supported_coins.dart @@ -0,0 +1,90 @@ +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/ethereum.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/litecoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/peercoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +/// The supported coins. Eventually move away from the Coin enum +class SupportedCoins { + /// A List of our supported coins. Piggy back on [Coin] for now + static final List cryptocurrencies = + Coin.values.map((e) => getCryptoCurrencyFor(e)).toList(growable: false); + + /// A getter function linking a [CryptoCurrency] with its associated [Coin]. + /// + /// Temporary: Remove when the Coin enum is removed. + static CryptoCurrency getCryptoCurrencyFor(Coin coin) { + switch (coin) { + case Coin.bitcoin: + return Bitcoin(CryptoCurrencyNetwork.main); + case Coin.bitcoinFrost: + return BitcoinFrost(CryptoCurrencyNetwork.main); + case Coin.litecoin: + return Litecoin(CryptoCurrencyNetwork.main); + case Coin.bitcoincash: + return Bitcoincash(CryptoCurrencyNetwork.main); + case Coin.dogecoin: + return Dogecoin(CryptoCurrencyNetwork.main); + case Coin.epicCash: + return Epiccash(CryptoCurrencyNetwork.main); + case Coin.eCash: + return Ecash(CryptoCurrencyNetwork.main); + case Coin.ethereum: + return Ethereum(CryptoCurrencyNetwork.main); + case Coin.firo: + return Firo(CryptoCurrencyNetwork.main); + case Coin.monero: + return Monero(CryptoCurrencyNetwork.main); + case Coin.particl: + return Particl(CryptoCurrencyNetwork.main); + case Coin.peercoin: + return Peercoin(CryptoCurrencyNetwork.main); + case Coin.solana: + return Solana(CryptoCurrencyNetwork.main); + case Coin.stellar: + return Stellar(CryptoCurrencyNetwork.main); + case Coin.tezos: + return Tezos(CryptoCurrencyNetwork.main); + case Coin.wownero: + return Wownero(CryptoCurrencyNetwork.main); + case Coin.namecoin: + return Namecoin(CryptoCurrencyNetwork.main); + case Coin.nano: + return Nano(CryptoCurrencyNetwork.main); + case Coin.banano: + return Banano(CryptoCurrencyNetwork.main); + case Coin.bitcoinTestNet: + return Bitcoin(CryptoCurrencyNetwork.test); + case Coin.bitcoinFrostTestNet: + return BitcoinFrost(CryptoCurrencyNetwork.test); + case Coin.litecoinTestNet: + return Litecoin(CryptoCurrencyNetwork.test); + case Coin.bitcoincashTestnet: + return Bitcoincash(CryptoCurrencyNetwork.test); + case Coin.firoTestNet: + return Firo(CryptoCurrencyNetwork.test); + case Coin.dogecoinTestNet: + return Dogecoin(CryptoCurrencyNetwork.test); + case Coin.stellarTestnet: + return Stellar(CryptoCurrencyNetwork.test); + case Coin.peercoinTestNet: + return Peercoin(CryptoCurrencyNetwork.test); + } + } +} diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index abec28d4e..e4dbcabc3 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -28,6 +28,8 @@ class CoinThemeColorDefault { Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); Color get particl => const Color(0xFF8175BD); + Color get peercoin => const Color(0xFF3CB054); + Color get solana => const Color(0xFFC696FF); Color get stellar => const Color(0xFF6600FF); Color get nano => const Color(0xFF209CE9); Color get banano => const Color(0xFFFBDD11); @@ -37,6 +39,8 @@ class CoinThemeColorDefault { switch (coin) { case Coin.bitcoin: case Coin.bitcoinTestNet: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: return bitcoin; case Coin.litecoin: case Coin.litecoinTestNet: @@ -64,6 +68,12 @@ class CoinThemeColorDefault { return wownero; case Coin.particl: return particl; + case Coin.peercoin: + return peercoin; + case Coin.peercoinTestNet: + return peercoin; + case Coin.solana: + return solana; case Coin.stellar: case Coin.stellarTestnet: return stellar; diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index cbec0077a..11e146d1b 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1680,6 +1680,8 @@ class StackColors extends ThemeExtension { switch (coin) { case Coin.bitcoin: case Coin.bitcoinTestNet: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: return _coin.bitcoin; case Coin.litecoin: case Coin.litecoinTestNet: @@ -1707,6 +1709,11 @@ class StackColors extends ThemeExtension { return _coin.wownero; case Coin.particl: return _coin.particl; + case Coin.peercoin: + case Coin.peercoinTestNet: + return _coin.peercoin; + case Coin.solana: + return _coin.solana; case Coin.stellar: case Coin.stellarTestnet: return _coin.stellar; diff --git a/lib/themes/theme_service.dart b/lib/themes/theme_service.dart index 01134da66..b5882ecc5 100644 --- a/lib/themes/theme_service.dart +++ b/lib/themes/theme_service.dart @@ -29,7 +29,7 @@ final pThemeService = Provider((ref) { }); class ThemeService { - static const _currentDefaultThemeVersion = 4; + static const _currentDefaultThemeVersion = 9; ThemeService._(); static ThemeService? _instance; static ThemeService get instance => _instance ??= ThemeService._(); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 0e766d28e..65e0231fe 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -10,14 +10,11 @@ import 'dart:convert'; -import 'package:bitcoindart/bitcoindart.dart'; -import 'package:crypto/crypto.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart'; -import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; - import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart'; @@ -29,49 +26,28 @@ import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +import '../wallets/crypto_currency/coins/peercoin.dart'; class AddressUtils { static String condenseAddress(String address) { return '${address.substring(0, 5)}...${address.substring(address.length - 5)}'; } - /// attempts to convert a string to a valid scripthash - /// - /// Returns the scripthash or throws an exception on invalid firo address - static String convertToScriptHash( - String address, - NetworkType network, [ - String overridePrefix = "", - ]) { - try { - final output = - Address.addressToOutputScript(address, network, overridePrefix); - final hash = sha256.convert(output.toList(growable: false)).toString(); - - final chars = hash.split(""); - final reversedPairs = []; - // TODO find a better/faster way to do this? - var i = chars.length - 1; - while (i > 0) { - reversedPairs.add(chars[i - 1]); - reversedPairs.add(chars[i]); - i -= 2; - } - return reversedPairs.join(""); - } catch (e) { - rethrow; - } - } - static bool validateAddress(String address, Coin coin) { //This calls the validate address for each crypto coin, validateAddress is //only used in 2 places, so I just replaced the old functionality here switch (coin) { case Coin.bitcoin: return Bitcoin(CryptoCurrencyNetwork.main).validateAddress(address); + case Coin.bitcoinFrost: + return BitcoinFrost(CryptoCurrencyNetwork.main) + .validateAddress(address); case Coin.litecoin: return Litecoin(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.bitcoincash: @@ -94,6 +70,10 @@ class AddressUtils { return Namecoin(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.particl: return Particl(CryptoCurrencyNetwork.main).validateAddress(address); + case Coin.peercoin: + return Peercoin(CryptoCurrencyNetwork.main).validateAddress(address); + case Coin.solana: + return Solana(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.stellar: return Stellar(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.nano: @@ -104,6 +84,9 @@ class AddressUtils { return Tezos(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.bitcoinTestNet: return Bitcoin(CryptoCurrencyNetwork.test).validateAddress(address); + case Coin.bitcoinFrostTestNet: + return BitcoinFrost(CryptoCurrencyNetwork.test) + .validateAddress(address); case Coin.litecoinTestNet: return Litecoin(CryptoCurrencyNetwork.test).validateAddress(address); case Coin.bitcoincashTestnet: @@ -112,6 +95,8 @@ class AddressUtils { return Firo(CryptoCurrencyNetwork.test).validateAddress(address); case Coin.dogecoinTestNet: return Dogecoin(CryptoCurrencyNetwork.test).validateAddress(address); + case Coin.peercoinTestNet: + return Peercoin(CryptoCurrencyNetwork.test).validateAddress(address); case Coin.stellarTestnet: return Stellar(CryptoCurrencyNetwork.test).validateAddress(address); } diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index 6a646fd11..e74de6510 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -39,12 +39,16 @@ enum AmountUnit { case Coin.firo: case Coin.litecoin: case Coin.particl: + case Coin.peercoin: case Coin.namecoin: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.dogecoinTestNet: case Coin.firoTestNet: + case Coin.peercoinTestNet: case Coin.bitcoin: case Coin.bitcoincash: case Coin.dogecoin: @@ -53,6 +57,7 @@ enum AmountUnit { case Coin.stellar: // TODO: check if this is correct case Coin.stellarTestnet: case Coin.tezos: + case Coin.solana: return AmountUnit.values.sublist(0, 4); case Coin.monero: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index ecd170f6c..9213fd1b2 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -143,6 +143,7 @@ class _SVG { String get chevronDown => "assets/svg/chevron-down.svg"; String get chevronUp => "assets/svg/chevron-up.svg"; String get swap => "assets/svg/swap.svg"; + String get swap2 => "assets/svg/swap2.svg"; String get downloadFolder => "assets/svg/folder-down.svg"; String get lock => "assets/svg/lock-keyhole.svg"; String get lockOpen => "assets/svg/lock-open.svg"; @@ -257,6 +258,7 @@ class _PNG { String get glasses => "assets/images/glasses.png"; String get glassesHidden => "assets/images/glasses-hidden.png"; + String get mascot => "assets/images/mascot.png"; } class _ANIMATIONS { diff --git a/lib/utilities/bip32_utils.dart b/lib/utilities/bip32_utils.dart index dcfdc329a..4129681a3 100644 --- a/lib/utilities/bip32_utils.dart +++ b/lib/utilities/bip32_utils.dart @@ -10,7 +10,6 @@ import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; -import 'package:bitcoindart/bitcoindart.dart'; import 'package:flutter/foundation.dart'; import 'package:tuple/tuple.dart'; @@ -19,25 +18,17 @@ abstract class Bip32Utils { static bip32.BIP32 getBip32RootSync( String mnemonic, String mnemonicPassphrase, - NetworkType networkType, + bip32.NetworkType networkType, ) { final seed = bip39.mnemonicToSeed(mnemonic, passphrase: mnemonicPassphrase); - final _networkType = bip32.NetworkType( - wif: networkType.wif, - bip32: bip32.Bip32Type( - public: networkType.bip32.public, - private: networkType.bip32.private, - ), - ); - - final root = bip32.BIP32.fromSeed(seed, _networkType); + final root = bip32.BIP32.fromSeed(seed, networkType); return root; } static Future getBip32Root( String mnemonic, String mnemonicPassphrase, - NetworkType networkType, + bip32.NetworkType networkType, ) async { final root = await compute( _getBip32RootWrapper, @@ -52,7 +43,7 @@ abstract class Bip32Utils { /// wrapper for compute() static bip32.BIP32 _getBip32RootWrapper( - Tuple3 args, + Tuple3 args, ) { return getBip32RootSync( args.item1, @@ -97,7 +88,7 @@ abstract class Bip32Utils { static bip32.BIP32 getBip32NodeSync( String mnemonic, String mnemonicPassphrase, - NetworkType network, + bip32.NetworkType network, String derivePath, ) { final root = getBip32RootSync(mnemonic, mnemonicPassphrase, network); @@ -109,7 +100,7 @@ abstract class Bip32Utils { static Future getBip32Node( String mnemonic, String mnemonicPassphrase, - NetworkType networkType, + bip32.NetworkType networkType, String derivePath, ) async { final node = await compute( @@ -126,7 +117,7 @@ abstract class Bip32Utils { /// wrapper for compute() static bip32.BIP32 _getBip32NodeWrapper( - Tuple4 args, + Tuple4 args, ) { return getBip32NodeSync( args.item1, diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index bb4ac06fb..cf67697b6 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -18,6 +18,7 @@ Uri getDefaultBlockExplorerUrlFor({ required String txid, }) { switch (coin) { + case Coin.bitcoinFrost: case Coin.bitcoin: return Uri.parse("https://mempool.space/tx/$txid"); case Coin.litecoin: @@ -25,11 +26,12 @@ Uri getDefaultBlockExplorerUrlFor({ case Coin.litecoinTestNet: return Uri.parse("https://chain.so/tx/LTCTEST/$txid"); case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: return Uri.parse("https://mempool.space/testnet/tx/$txid"); case Coin.dogecoin: return Uri.parse("https://chain.so/tx/DOGE/$txid"); case Coin.eCash: - return Uri.parse("https://explorer.bitcoinabc.org/tx/$txid"); + return Uri.parse("https://explorer.e.cash/tx/$txid"); case Coin.dogecoinTestNet: return Uri.parse("https://chain.so/tx/DOGETEST/$txid"); case Coin.epicCash: @@ -64,6 +66,13 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://testnet.stellarchain.io/transactions/$txid"); case Coin.tezos: return Uri.parse("https://tzstats.com/$txid"); + case Coin.solana: + return Uri.parse("https://explorer.solana.com/tx/$txid"); + case Coin.peercoin: + return Uri.parse("https://chainz.cryptoid.info/ppc/tx.dws?$txid.htm"); + case Coin.peercoinTestNet: + return Uri.parse( + "https://chainz.cryptoid.info/ppc-test/search.dws?q=$txid.htm"); } } diff --git a/lib/utilities/connection_check/electrum_connection_check.dart b/lib/utilities/connection_check/electrum_connection_check.dart new file mode 100644 index 000000000..1dbc003c8 --- /dev/null +++ b/lib/utilities/connection_check/electrum_connection_check.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:electrum_adapter/electrum_adapter.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +Future checkElectrumServer({ + required String host, + required int port, + required bool useSSL, + Prefs? overridePrefs, + TorService? overrideTorService, +}) async { + final _prefs = overridePrefs ?? Prefs.instance; + final _torService = overrideTorService ?? TorService.sharedInstance; + + ({InternetAddress host, int port})? proxyInfo; + + try { + // If we're supposed to use Tor... + if (_prefs.useTor) { + // But Tor isn't running... + if (_torService.status != TorConnectionStatus.connected) { + // And the killswitch isn't set... + if (!_prefs.torKillSwitch) { + // Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function. + Logging.instance.log( + "Tor preference set but Tor is not enabled, killswitch not set, connecting to Electrum adapter through clearnet", + level: LogLevel.Warning, + ); + } else { + // ... But if the killswitch is set, then we throw an exception. + throw Exception( + "Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter"); + // TODO [prio=low]: Try to start Tor. + } + } else { + // Get the proxy info from the TorService. + proxyInfo = _torService.getProxyInfo(); + } + } + + final client = await ElectrumClient.connect( + host: host, + port: port, + useSSL: useSSL, + proxyInfo: proxyInfo, + ).timeout( + const Duration(seconds: 5), + onTimeout: () => throw Exception( + "The checkElectrumServer connect() call timed out.", + ), + ); + + await client.ping().timeout(const Duration(seconds: 5)); + + return true; + } catch (_) { + return false; + } +} diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index db0543044..d3fedc666 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -46,6 +46,8 @@ abstract class Constants { 10000000); // https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/assets#amount-precision static final BigInt _satsPerCoin = BigInt.from(100000000); static final BigInt _satsPerCoinTezos = BigInt.from(1000000); + static final BigInt _satsPerCoinSolana = BigInt.from(1000000000); + static final BigInt _satsPerCoinPeercoin = BigInt.from(1000000); // 1*10^6. static const int _decimalPlaces = 8; static const int _decimalPlacesNano = 30; static const int _decimalPlacesBanano = 29; @@ -55,6 +57,8 @@ abstract class Constants { static const int _decimalPlacesECash = 2; static const int _decimalPlacesStellar = 7; static const int _decimalPlacesTezos = 6; + static const int _decimalPlacesSolana = 9; + static const int _decimalPlacesPeercoin = 6; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -69,6 +73,7 @@ abstract class Constants { static BigInt satsPerCoin(Coin coin) { switch (coin) { case Coin.bitcoin: + case Coin.bitcoinFrost: case Coin.litecoin: case Coin.litecoinTestNet: case Coin.bitcoincash: @@ -76,6 +81,7 @@ abstract class Constants { case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: @@ -107,12 +113,20 @@ abstract class Constants { case Coin.tezos: return _satsPerCoinTezos; + + case Coin.solana: + return _satsPerCoinSolana; + + case Coin.peercoin: + case Coin.peercoinTestNet: + return _satsPerCoinPeercoin; } } static int decimalPlacesForCoin(Coin coin) { switch (coin) { case Coin.bitcoin: + case Coin.bitcoinFrost: case Coin.litecoin: case Coin.litecoinTestNet: case Coin.bitcoincash: @@ -120,6 +134,7 @@ abstract class Constants { case Coin.dogecoin: case Coin.firo: case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: @@ -151,6 +166,13 @@ abstract class Constants { case Coin.tezos: return _decimalPlacesTezos; + + case Coin.solana: + return _decimalPlacesSolana; + + case Coin.peercoin: + case Coin.peercoinTestNet: + return _decimalPlacesPeercoin; } } @@ -172,6 +194,9 @@ abstract class Constants { case Coin.ethereum: case Coin.namecoin: case Coin.particl: + values.addAll([12, 24]); + break; + case Coin.solana: case Coin.nano: case Coin.stellar: case Coin.stellarTestnet: @@ -189,6 +214,14 @@ abstract class Constants { case Coin.wownero: values.addAll([14, 25]); break; + + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + throw ArgumentError("Frost mnemonic lengths unsupported"); + case Coin.peercoin: + case Coin.peercoinTestNet: + values.addAll([12, /*15, 18, 21,*/ 24]); // TODO [prio=low]: Test rest. + break; } return values; } @@ -198,9 +231,13 @@ abstract class Constants { switch (coin) { case Coin.bitcoin: case Coin.bitcoinTestNet: + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.bitcoincash: case Coin.bitcoincashTestnet: case Coin.eCash: + case Coin.peercoin: + case Coin.peercoinTestNet: return 600; case Coin.dogecoin: @@ -235,6 +272,7 @@ abstract class Constants { case Coin.nano: // TODO: Verify this case Coin.banano: // TODO: Verify this + case Coin.solana: return 1; case Coin.stellar: @@ -262,6 +300,7 @@ abstract class Constants { case Coin.namecoin: case Coin.particl: case Coin.ethereum: + case Coin.solana: return 12; case Coin.wownero: @@ -270,6 +309,8 @@ abstract class Constants { case Coin.nano: case Coin.banano: case Coin.epicCash: + case Coin.peercoin: // TODO [prio=low]: Verify default seed length. + case Coin.peercoinTestNet: case Coin.stellar: case Coin.stellarTestnet: case Coin.tezos: @@ -277,6 +318,10 @@ abstract class Constants { case Coin.monero: return 25; + + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + throw ArgumentError("Frost mnemonic length unsupported"); // // default: // -1; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 5d80784a3..2e2bd71cc 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -17,30 +17,9 @@ abstract class DefaultNodes { static const String defaultName = "Stack Default"; @Deprecated("old and decrepit") - static List get all => [ - bitcoin, - litecoin, - dogecoin, - firo, - monero, - eCash, - epicCash, - ethereum, - bitcoincash, - namecoin, - wownero, - particl, - stellar, - nano, - banano, - tezos, - bitcoinTestnet, - litecoinTestNet, - bitcoincashTestnet, - dogecoinTestnet, - firoTestnet, - stellarTestnet, - ]; + static List get all => Coin.values + .map((e) => DefaultNodes.getNodeFor(e)) + .toList(growable: false); static NodeModel get bitcoin => NodeModel( host: "bitcoin.stackwallet.com", @@ -188,6 +167,31 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get peercoin => NodeModel( + host: "electrum.peercoinexplorer.net", + port: 50002, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.peercoin), + useSSL: true, + enabled: true, + coinName: Coin.peercoin.name, + isFailover: true, + isDown: false, + ); + + static NodeModel get solana => NodeModel( + host: + "https://api.mainnet-beta.solana.com", // TODO: Change this to stack wallet one + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.solana), + useSSL: true, + enabled: true, + coinName: Coin.solana.name, + isFailover: true, + isDown: false, + ); + static NodeModel get stellar => NodeModel( host: "https://horizon.stellar.org", port: 443, @@ -297,6 +301,18 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get peercoinTestNet => NodeModel( + host: "testnet-electrum.peercoinexplorer.net", + port: 50002, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.peercoinTestNet), + useSSL: true, + enabled: true, + coinName: Coin.peercoinTestNet.name, + isFailover: true, + isDown: false, + ); + static NodeModel get stellarTestnet => NodeModel( host: "https://horizon-testnet.stellar.org/", port: 50022, @@ -312,6 +328,7 @@ abstract class DefaultNodes { static NodeModel getNodeFor(Coin coin) { switch (coin) { case Coin.bitcoin: + case Coin.bitcoinFrost: return bitcoin; case Coin.litecoin: @@ -347,6 +364,15 @@ abstract class DefaultNodes { case Coin.particl: return particl; + case Coin.peercoin: + return peercoin; + + case Coin.peercoinTestNet: + return peercoinTestNet; + + case Coin.solana: + return solana; + case Coin.stellar: return stellar; @@ -360,6 +386,7 @@ abstract class DefaultNodes { return tezos; case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: return bitcoinTestnet; case Coin.litecoinTestNet: diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index c71d39ba4..903054b09 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -16,6 +16,7 @@ enum Coin { monero, banano, bitcoincash, + bitcoinFrost, dogecoin, eCash, epicCash, @@ -25,6 +26,8 @@ enum Coin { namecoin, nano, particl, + peercoin, + solana, stellar, tezos, wownero, @@ -36,9 +39,11 @@ enum Coin { bitcoinTestNet, bitcoincashTestnet, + bitcoinFrostTestNet, dogecoinTestNet, firoTestNet, litecoinTestNet, + peercoinTestNet, stellarTestnet, } @@ -47,6 +52,8 @@ extension CoinExt on Coin { switch (this) { case Coin.bitcoin: return "Bitcoin"; + case Coin.bitcoinFrost: + return "Bitcoin Frost"; case Coin.litecoin: return "Litecoin"; case Coin.bitcoincash: @@ -65,6 +72,10 @@ extension CoinExt on Coin { return "Monero"; case Coin.particl: return "Particl"; + case Coin.peercoin: + return "Peercoin"; + case Coin.solana: + return "Solana"; case Coin.stellar: return "Stellar"; case Coin.tezos: @@ -79,6 +90,8 @@ extension CoinExt on Coin { return "Banano"; case Coin.bitcoinTestNet: return "tBitcoin"; + case Coin.bitcoinFrostTestNet: + return "tBitcoin Frost"; case Coin.litecoinTestNet: return "tLitecoin"; case Coin.bitcoincashTestnet: @@ -87,6 +100,8 @@ extension CoinExt on Coin { return "tFiro"; case Coin.dogecoinTestNet: return "tDogecoin"; + case Coin.peercoinTestNet: + return "tPeercoin"; case Coin.stellarTestnet: return "tStellar"; } @@ -95,6 +110,7 @@ extension CoinExt on Coin { String get ticker { switch (this) { case Coin.bitcoin: + case Coin.bitcoinFrost: return "BTC"; case Coin.litecoin: return "LTC"; @@ -114,6 +130,10 @@ extension CoinExt on Coin { return "XMR"; case Coin.particl: return "PART"; + case Coin.peercoin: + return "PPC"; + case Coin.solana: + return "SOL"; case Coin.stellar: return "XLM"; case Coin.tezos: @@ -127,6 +147,7 @@ extension CoinExt on Coin { case Coin.banano: return "BAN"; case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: return "tBTC"; case Coin.litecoinTestNet: return "tLTC"; @@ -136,6 +157,8 @@ extension CoinExt on Coin { return "tFIRO"; case Coin.dogecoinTestNet: return "tDOGE"; + case Coin.peercoinTestNet: + return "tPPC"; case Coin.stellarTestnet: return "tXLM"; } @@ -144,6 +167,7 @@ extension CoinExt on Coin { String get uriScheme { switch (this) { case Coin.bitcoin: + case Coin.bitcoinFrost: return "bitcoin"; case Coin.litecoin: return "litecoin"; @@ -164,6 +188,10 @@ extension CoinExt on Coin { return "monero"; case Coin.particl: return "particl"; + case Coin.peercoin: + return "peercoin"; + case Coin.solana: + return "solana"; case Coin.stellar: return "stellar"; case Coin.tezos: @@ -177,6 +205,7 @@ extension CoinExt on Coin { case Coin.banano: return "ban"; case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: return "bitcoin"; case Coin.litecoinTestNet: return "litecoin"; @@ -186,41 +215,13 @@ extension CoinExt on Coin { return "firo"; case Coin.dogecoinTestNet: return "dogecoin"; + case Coin.peercoinTestNet: + return "peercoin"; case Coin.stellarTestnet: return "stellar"; } } - bool get isElectrumXCoin { - switch (this) { - case Coin.bitcoin: - case Coin.litecoin: - case Coin.bitcoincash: - case Coin.dogecoin: - case Coin.firo: - case Coin.namecoin: - case Coin.particl: - case Coin.bitcoinTestNet: - case Coin.litecoinTestNet: - case Coin.bitcoincashTestnet: - case Coin.firoTestNet: - case Coin.dogecoinTestNet: - case Coin.eCash: - return true; - - case Coin.epicCash: - case Coin.ethereum: - case Coin.monero: - case Coin.tezos: - case Coin.wownero: - case Coin.nano: - case Coin.banano: - case Coin.stellar: - case Coin.stellarTestnet: - return false; - } - } - bool get hasMnemonicPassphraseSupport { switch (this) { case Coin.bitcoin: @@ -235,18 +236,23 @@ extension CoinExt on Coin { case Coin.firoTestNet: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.ethereum: case Coin.eCash: case Coin.stellar: case Coin.stellarTestnet: return true; + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.epicCash: case Coin.monero: case Coin.wownero: case Coin.nano: case Coin.banano: case Coin.tezos: + case Coin.solana: return false; } } @@ -260,9 +266,13 @@ extension CoinExt on Coin { case Coin.ethereum: return true; + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.eCash: case Coin.epicCash: case Coin.monero: @@ -275,6 +285,7 @@ extension CoinExt on Coin { case Coin.firoTestNet: case Coin.nano: case Coin.banano: + case Coin.solana: case Coin.stellar: case Coin.stellarTestnet: return false; @@ -284,12 +295,14 @@ extension CoinExt on Coin { bool get isTestNet { switch (this) { case Coin.bitcoin: + case Coin.bitcoinFrost: case Coin.litecoin: case Coin.bitcoincash: case Coin.dogecoin: case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: case Coin.epicCash: case Coin.ethereum: case Coin.monero: @@ -299,27 +312,43 @@ extension CoinExt on Coin { case Coin.banano: case Coin.eCash: case Coin.stellar: + case Coin.solana: return false; case Coin.dogecoinTestNet: case Coin.bitcoinTestNet: + case Coin.bitcoinFrostTestNet: case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: + case Coin.peercoinTestNet: case Coin.stellarTestnet: return true; } } + bool get isFrost { + switch (this) { + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + return true; + + default: + return false; + } + } + Coin get mainNetVersion { switch (this) { case Coin.bitcoin: + case Coin.bitcoinFrost: case Coin.litecoin: case Coin.bitcoincash: case Coin.dogecoin: case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: case Coin.epicCash: case Coin.ethereum: case Coin.monero: @@ -329,6 +358,7 @@ extension CoinExt on Coin { case Coin.banano: case Coin.eCash: case Coin.stellar: + case Coin.solana: return this; case Coin.dogecoinTestNet: @@ -337,6 +367,9 @@ extension CoinExt on Coin { case Coin.bitcoinTestNet: return Coin.bitcoin; + case Coin.bitcoinFrostTestNet: + return Coin.bitcoinFrost; + case Coin.litecoinTestNet: return Coin.litecoin; @@ -346,6 +379,9 @@ extension CoinExt on Coin { case Coin.firoTestNet: return Coin.firo; + case Coin.peercoinTestNet: + return Coin.peercoin; + case Coin.stellarTestnet: return Coin.stellar; } @@ -362,8 +398,14 @@ extension CoinExt on Coin { case Coin.litecoinTestNet: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: return AddressType.p2wpkh; + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + return AddressType.frostMS; + case Coin.eCash: case Coin.bitcoincash: case Coin.bitcoincashTestnet: @@ -395,6 +437,9 @@ extension CoinExt on Coin { case Coin.stellar: case Coin.stellarTestnet: return AddressType.stellar; + + case Coin.solana: + return AddressType.solana; } } } @@ -443,6 +488,19 @@ Coin coinFromPrettyName(String name) { case "particl": return Coin.particl; + case "Peercoin": + case "peercoin": + return Coin.peercoin; + + case "tPeercoin": + case "Peercoin Testnet": + case "peercoinTestNet": + return Coin.peercoinTestNet; + + case "Solana": + case "solana": + return Coin.solana; + case "Stellar": case "stellar": return Coin.stellar; @@ -501,6 +559,15 @@ Coin coinFromPrettyName(String name) { case "tStellar": return Coin.stellarTestnet; + case "Bitcoin Frost": + case "bitcoinFrost": + return Coin.bitcoinFrost; + + case "Bitcoin Frost Testnet": + case "tBitcoin Frost": + case "bitcoinFrostTestNet": + return Coin.bitcoinFrostTestNet; + default: throw ArgumentError.value( name, @@ -534,6 +601,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.namecoin; case "part": return Coin.particl; + case "sol": + return Coin.solana; case "xlm": return Coin.stellar; case "xtz": diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 5b94f41f6..4dcaef022 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -8,6 +8,7 @@ * */ +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; enum DerivePathType { @@ -17,6 +18,32 @@ enum DerivePathType { bip84, eth, eCash44, + solana, + bip86; + + AddressType getAddressType() { + switch (this) { + case DerivePathType.bip44: + case DerivePathType.bch44: + case DerivePathType.eCash44: + return AddressType.p2pkh; + + case DerivePathType.bip49: + return AddressType.p2sh; + + case DerivePathType.bip84: + return AddressType.p2wpkh; + + case DerivePathType.eth: + return AddressType.ethereum; + + case DerivePathType.solana: + return AddressType.solana; + + case DerivePathType.bip86: + return AddressType.p2tr; + } + } } extension DerivePathTypeExt on DerivePathType { @@ -36,6 +63,8 @@ extension DerivePathTypeExt on DerivePathType { case Coin.litecoinTestNet: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: return DerivePathType.bip84; case Coin.eCash: @@ -44,6 +73,11 @@ extension DerivePathTypeExt on DerivePathType { case Coin.ethereum: // TODO: do we need something here? return DerivePathType.eth; + case Coin.solana: + return DerivePathType.solana; + + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: case Coin.epicCash: case Coin.monero: case Coin.wownero: diff --git a/lib/utilities/extensions/extensions.dart b/lib/utilities/extensions/extensions.dart index 4a9aac287..a798c002a 100644 --- a/lib/utilities/extensions/extensions.dart +++ b/lib/utilities/extensions/extensions.dart @@ -9,5 +9,9 @@ */ export 'impl/big_int.dart'; +export 'impl/box_shadow.dart'; +export 'impl/cl_transaction.dart'; +export 'impl/contract_abi.dart'; +export 'impl/gradient.dart'; export 'impl/string.dart'; export 'impl/uint8_list.dart'; diff --git a/lib/utilities/extensions/impl/cl_transaction.dart b/lib/utilities/extensions/impl/cl_transaction.dart new file mode 100644 index 000000000..1c117cf8c --- /dev/null +++ b/lib/utilities/extensions/impl/cl_transaction.dart @@ -0,0 +1,53 @@ +import 'dart:typed_data'; + +import 'package:coinlib_flutter/coinlib_flutter.dart'; + +extension CLTransactionExt on Transaction { + int weight() { + final base = _byteLength(false); + final total = _byteLength(true); + return base * 3 + total; + } + + int vSize() => (weight() / 4).ceil(); + + int _byteLength(final bool allowWitness) { + final hasWitness = allowWitness && isWitness; + return (hasWitness ? 10 : 8) + + _encodingLength(inputs.length) + + _encodingLength(outputs.length) + + inputs.fold(0, (sum, input) => sum + input.size) + + outputs.fold(0, (sum, output) => sum + output.size) + + (hasWitness + ? inputs.fold(0, (sum, input) { + if (input is! WitnessInput) { + return sum; + } else { + return sum + _vectorSize(input.witness); + } + }) + : 0); + } + + int _varSliceSize(Uint8List someScript) { + final length = someScript.length; + return _encodingLength(length) + length; + } + + int _vectorSize(List someVector) { + final length = someVector.length; + return _encodingLength(length) + + someVector.fold( + 0, + (sum, witness) => sum + _varSliceSize(witness), + ); + } + + int _encodingLength(int number) => number < 0xfd + ? 1 + : number <= 0xffff + ? 3 + : number <= 0xffffffff + ? 5 + : 9; +} diff --git a/lib/utilities/show_loading.dart b/lib/utilities/show_loading.dart index 759eaa05c..e01a86441 100644 --- a/lib/utilities/show_loading.dart +++ b/lib/utilities/show_loading.dart @@ -20,7 +20,7 @@ Future showLoading({ required BuildContext context, required String message, String? subMessage, - bool isDesktop = false, + bool rootNavigator = false, bool opaqueBG = false, void Function(Exception)? onException, }) async { @@ -59,7 +59,7 @@ Future showLoading({ } if (context.mounted) { - Navigator.of(context, rootNavigator: isDesktop).pop(); + Navigator.of(context, rootNavigator: rootNavigator).pop(); if (ex != null) { onException?.call(ex); } diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index fab828bef..0a7383e9f 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -317,6 +317,28 @@ class STextStyles { } } + static TextStyle w600_18(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 18, + ); + } + } + + static TextStyle w500_16(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + } + } + static TextStyle w500_14(BuildContext context) { switch (_theme(context).themeId) { default: @@ -361,6 +383,28 @@ class STextStyles { } } + static TextStyle w400_16(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 16, + ); + } + } + + static TextStyle w400_14(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w400, + fontSize: 14, + ); + } + } + static TextStyle w600_20(BuildContext context) { switch (_theme(context).themeId) { default: diff --git a/lib/wallets/crypto_currency/coins/banano.dart b/lib/wallets/crypto_currency/coins/banano.dart index 8f980e947..dfae1b78c 100644 --- a/lib/wallets/crypto_currency/coins/banano.dart +++ b/lib/wallets/crypto_currency/coins/banano.dart @@ -45,4 +45,12 @@ class Banano extends NanoCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Banano && other.network == network; + } + + @override + int get hashCode => Object.hash(Banano, network); } diff --git a/lib/wallets/crypto_currency/coins/bitcoin.dart b/lib/wallets/crypto_currency/coins/bitcoin.dart index 2402a977f..e0126ede1 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin.dart @@ -25,11 +25,15 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { // change this to change the number of confirms a tx needs in order to show as confirmed int get minConfirms => 1; + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, DerivePathType.bip49, DerivePathType.bip84, + DerivePathType.bip86, // P2TR. ]; @override @@ -51,10 +55,10 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x80, p2pkhPrefix: 0x00, p2shPrefix: 0x05, @@ -62,9 +66,12 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { pubHDPrefix: 0x0488b21e, bech32Hrp: "bc", messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xef, p2pkhPrefix: 0x6f, p2shPrefix: 0xc4, @@ -72,6 +79,9 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { pubHDPrefix: 0x043587cf, bech32Hrp: "tb", messagePrefix: "\x18Bitcoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); @@ -109,6 +119,9 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { case DerivePathType.bip84: purpose = 84; break; + case DerivePathType.bip86: + purpose = 86; + break; default: throw Exception("DerivePathType $derivePathType not supported"); } @@ -130,13 +143,14 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { return (address: addr, addressType: AddressType.p2pkh); + // TODO: [prio=high] verify this works similarly to bitcoindart's p2sh or something(!!) case DerivePathType.bip49: final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( publicKey, hrp: networkParams.bech32Hrp, ).program.script; - final addr = coinlib.P2SHAddress.fromScript( + final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, version: networkParams.p2shPrefix, ); @@ -151,6 +165,16 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { return (address: addr, addressType: AddressType.p2wpkh); + case DerivePathType.bip86: + final taproot = coinlib.Taproot(internalKey: publicKey); + + final addr = coinlib.P2TRAddress.fromTaproot( + taproot, + hrp: networkParams.bech32Hrp, + ); + + return (address: addr, addressType: AddressType.p2tr); + default: throw Exception("DerivePathType $derivePathType not supported"); } @@ -170,33 +194,21 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface { NodeModel get defaultNode { switch (network) { case CryptoCurrencyNetwork.main: - return NodeModel( - host: "bitcoin.stackwallet.com", - port: 50002, - name: DefaultNodes.defaultName, - id: DefaultNodes.buildId(Coin.bitcoin), - useSSL: true, - enabled: true, - coinName: Coin.bitcoin.name, - isFailover: true, - isDown: false, - ); + return DefaultNodes.bitcoin; case CryptoCurrencyNetwork.test: - return NodeModel( - host: "bitcoin-testnet.stackwallet.com", - port: 51002, - name: DefaultNodes.defaultName, - id: DefaultNodes.buildId(Coin.bitcoinTestNet), - useSSL: true, - enabled: true, - coinName: Coin.bitcoinTestNet.name, - isFailover: true, - isDown: false, - ); + return DefaultNodes.bitcoinTestnet; default: throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Bitcoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Bitcoin, network); } diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart new file mode 100644 index 000000000..bd5216350 --- /dev/null +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -0,0 +1,83 @@ +import 'dart:typed_data'; + +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; + +class BitcoinFrost extends FrostCurrency { + BitcoinFrost(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.bitcoinFrost; + case CryptoCurrencyNetwork.test: + coin = Coin.bitcoinFrostTestNet; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 1; + + @override + bool get torSupport => true; + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return DefaultNodes.bitcoin; + + case CryptoCurrencyNetwork.test: + return DefaultNodes.bitcoinTestnet; + + default: + throw UnimplementedError(); + } + } + + @override + String get genesisHash { + switch (network) { + case CryptoCurrencyNetwork.main: + return "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; + case CryptoCurrencyNetwork.test: + return "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + Amount get dustLimit => Amount( + rawValue: BigInt.from(294), + fractionDigits: fractionDigits, + ); + + @override + String pubKeyToScriptHash({required Uint8List pubKey}) { + try { + return Bip39HDCurrency.convertBytesToScriptHash(pubKey); + } catch (e) { + rethrow; + } + } + + @override + bool validateAddress(String address) { + // TODO: implement validateAddress for frost addresses + return true; + } + + @override + bool operator ==(Object other) { + return other is BitcoinFrost && other.network == network; + } + + @override + int get hashCode => Object.hash(BitcoinFrost, network); +} diff --git a/lib/wallets/crypto_currency/coins/bitcoincash.dart b/lib/wallets/crypto_currency/coins/bitcoincash.dart index eaf40d4d0..168e0223a 100644 --- a/lib/wallets/crypto_currency/coins/bitcoincash.dart +++ b/lib/wallets/crypto_currency/coins/bitcoincash.dart @@ -34,6 +34,9 @@ class Bitcoincash extends Bip39HDCurrency { // change this to change the number of confirms a tx needs in order to show as confirmed int get minConfirms => 0; // bch zeroconf + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, @@ -59,10 +62,10 @@ class Bitcoincash extends Bip39HDCurrency { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x80, p2pkhPrefix: 0x00, p2shPrefix: 0x05, @@ -70,9 +73,12 @@ class Bitcoincash extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "bc", messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xef, p2pkhPrefix: 0x6f, p2shPrefix: 0xc4, @@ -80,6 +86,9 @@ class Bitcoincash extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tb", messagePrefix: "\x18Bitcoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); @@ -280,4 +289,12 @@ class Bitcoincash extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Bitcoincash && other.network == network; + } + + @override + int get hashCode => Object.hash(Bitcoincash, network); } diff --git a/lib/wallets/crypto_currency/coins/dogecoin.dart b/lib/wallets/crypto_currency/coins/dogecoin.dart index 2051839e4..26abdaa85 100644 --- a/lib/wallets/crypto_currency/coins/dogecoin.dart +++ b/lib/wallets/crypto_currency/coins/dogecoin.dart @@ -20,6 +20,9 @@ class Dogecoin extends Bip39HDCurrency { } } + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, @@ -102,10 +105,10 @@ class Dogecoin extends Bip39HDCurrency { int get minConfirms => 1; @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x9e, p2pkhPrefix: 0x1e, p2shPrefix: 0x16, @@ -113,9 +116,12 @@ class Dogecoin extends Bip39HDCurrency { pubHDPrefix: 0x02facafd, bech32Hrp: "doge", messagePrefix: '\x18Dogecoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xf1, p2pkhPrefix: 0x71, p2shPrefix: 0xc4, @@ -123,6 +129,9 @@ class Dogecoin extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tdge", messagePrefix: "\x18Dogecoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); @@ -172,4 +181,12 @@ class Dogecoin extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Dogecoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Dogecoin, network); } diff --git a/lib/wallets/crypto_currency/coins/ecash.dart b/lib/wallets/crypto_currency/coins/ecash.dart index 07e164c6e..6c068ed4d 100644 --- a/lib/wallets/crypto_currency/coins/ecash.dart +++ b/lib/wallets/crypto_currency/coins/ecash.dart @@ -32,6 +32,9 @@ class Ecash extends Bip39HDCurrency { // change this to change the number of confirms a tx needs in order to show as confirmed int get minConfirms => 0; // bch zeroconf + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.eCash44, @@ -57,10 +60,10 @@ class Ecash extends Bip39HDCurrency { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x80, p2pkhPrefix: 0x00, p2shPrefix: 0x05, @@ -68,9 +71,12 @@ class Ecash extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "bc", messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xef, p2pkhPrefix: 0x6f, p2shPrefix: 0xc4, @@ -78,6 +84,9 @@ class Ecash extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tb", messagePrefix: "\x18Bitcoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); @@ -260,4 +269,12 @@ class Ecash extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Ecash && other.network == network; + } + + @override + int get hashCode => Object.hash(Ecash, network); } diff --git a/lib/wallets/crypto_currency/coins/epiccash.dart b/lib/wallets/crypto_currency/coins/epiccash.dart index 0f4e77af2..49d7a7a7f 100644 --- a/lib/wallets/crypto_currency/coins/epiccash.dart +++ b/lib/wallets/crypto_currency/coins/epiccash.dart @@ -61,4 +61,12 @@ class Epiccash extends Bip39Currency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Epiccash && other.network == network; + } + + @override + int get hashCode => Object.hash(Epiccash, network); } diff --git a/lib/wallets/crypto_currency/coins/ethereum.dart b/lib/wallets/crypto_currency/coins/ethereum.dart index 624fcd3d2..a9bc81380 100644 --- a/lib/wallets/crypto_currency/coins/ethereum.dart +++ b/lib/wallets/crypto_currency/coins/ethereum.dart @@ -34,4 +34,12 @@ class Ethereum extends Bip39Currency { bool validateAddress(String address) { return isValidEthereumAddress(address); } + + @override + bool operator ==(Object other) { + return other is Ethereum && other.network == network; + } + + @override + int get hashCode => Object.hash(Ethereum, network); } diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index 82eb8ab39..36ad7d763 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -24,6 +24,9 @@ class Firo extends Bip39HDCurrency { @override int get minConfirms => 1; + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, @@ -48,10 +51,10 @@ class Firo extends Bip39HDCurrency { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xd2, p2pkhPrefix: 0x52, p2shPrefix: 0x07, @@ -59,9 +62,12 @@ class Firo extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "bc", messagePrefix: '\x18Zcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xb9, p2pkhPrefix: 0x41, p2shPrefix: 0xb2, @@ -69,6 +75,9 @@ class Firo extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tb", messagePrefix: "\x18Zcoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); @@ -190,4 +199,12 @@ class Firo extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Firo && other.network == network; + } + + @override + int get hashCode => Object.hash(Firo, network); } diff --git a/lib/wallets/crypto_currency/coins/litecoin.dart b/lib/wallets/crypto_currency/coins/litecoin.dart index 5c964db5a..efccbb3de 100644 --- a/lib/wallets/crypto_currency/coins/litecoin.dart +++ b/lib/wallets/crypto_currency/coins/litecoin.dart @@ -24,6 +24,9 @@ class Litecoin extends Bip39HDCurrency { // change this to change the number of confirms a tx needs in order to show as confirmed int get minConfirms => 1; + @override + bool get torSupport => true; + @override List get supportedDerivationPathTypes => [ DerivePathType.bip44, @@ -55,10 +58,10 @@ class Litecoin extends Bip39HDCurrency { ); @override - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xb0, p2pkhPrefix: 0x30, p2shPrefix: 0x32, @@ -66,9 +69,12 @@ class Litecoin extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "ltc", messagePrefix: '\x19Litecoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); case CryptoCurrencyNetwork.test: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xef, p2pkhPrefix: 0x6f, p2shPrefix: 0x3a, @@ -76,6 +82,9 @@ class Litecoin extends Bip39HDCurrency { pubHDPrefix: 0x043587cf, bech32Hrp: "tltc", messagePrefix: "\x19Litecoin Signed Message:\n", + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); default: throw Exception("Unsupported network: $network"); @@ -140,7 +149,7 @@ class Litecoin extends Bip39HDCurrency { hrp: networkParams.bech32Hrp, ).program.script; - final addr = coinlib.P2SHAddress.fromScript( + final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, version: networkParams.p2shPrefix, ); @@ -203,4 +212,12 @@ class Litecoin extends Bip39HDCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Litecoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Litecoin, network); } diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 748d1b7eb..34eff8203 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -44,4 +44,12 @@ class Monero extends CryptonoteCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Monero && other.network == network; + } + + @override + int get hashCode => Object.hash(Monero, network); } diff --git a/lib/wallets/crypto_currency/coins/namecoin.dart b/lib/wallets/crypto_currency/coins/namecoin.dart index ce5906ee3..c2cff8e57 100644 --- a/lib/wallets/crypto_currency/coins/namecoin.dart +++ b/lib/wallets/crypto_currency/coins/namecoin.dart @@ -22,6 +22,9 @@ class Namecoin extends Bip39HDCurrency { // See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L58 int get minConfirms => 2; + @override + bool get torSupport => true; + @override // See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L80 String constructDerivePath({ @@ -121,7 +124,7 @@ class Namecoin extends Bip39HDCurrency { hrp: networkParams.bech32Hrp, ).program.script; - final addr = coinlib.P2SHAddress.fromScript( + final addr = coinlib.P2SHAddress.fromRedeemScript( p2wpkhScript, version: networkParams.p2shPrefix, ); @@ -143,10 +146,10 @@ class Namecoin extends Bip39HDCurrency { @override // See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L3474 - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0xb4, // From 180. p2pkhPrefix: 0x34, // From 52. p2shPrefix: 0x0d, // From 13. @@ -154,6 +157,9 @@ class Namecoin extends Bip39HDCurrency { pubHDPrefix: 0x0488b21e, bech32Hrp: "nc", messagePrefix: '\x18Namecoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); // case CryptoCurrencyNetwork.test: // TODO: [prio=low] Add testnet support. @@ -179,4 +185,12 @@ class Namecoin extends Bip39HDCurrency { return false; } } + + @override + bool operator ==(Object other) { + return other is Namecoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Namecoin, network); } diff --git a/lib/wallets/crypto_currency/coins/nano.dart b/lib/wallets/crypto_currency/coins/nano.dart index 016a3b796..277a4601f 100644 --- a/lib/wallets/crypto_currency/coins/nano.dart +++ b/lib/wallets/crypto_currency/coins/nano.dart @@ -45,4 +45,12 @@ class Nano extends NanoCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Nano && other.network == network; + } + + @override + int get hashCode => Object.hash(Nano, network); } diff --git a/lib/wallets/crypto_currency/coins/particl.dart b/lib/wallets/crypto_currency/coins/particl.dart index 32da6e1f3..ae73d46e5 100644 --- a/lib/wallets/crypto_currency/coins/particl.dart +++ b/lib/wallets/crypto_currency/coins/particl.dart @@ -22,6 +22,9 @@ class Particl extends Bip39HDCurrency { // See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L57 int get minConfirms => 1; + @override + bool get torSupport => true; + @override // See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L68 String constructDerivePath( @@ -125,10 +128,10 @@ class Particl extends Bip39HDCurrency { @override // See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L3532 - coinlib.NetworkParams get networkParams { + coinlib.Network get networkParams { switch (network) { case CryptoCurrencyNetwork.main: - return const coinlib.NetworkParams( + return coinlib.Network( wifPrefix: 0x6c, p2pkhPrefix: 0x38, p2shPrefix: 0x3c, @@ -136,6 +139,9 @@ class Particl extends Bip39HDCurrency { pubHDPrefix: 0x696e82d1, bech32Hrp: "pw", messagePrefix: '\x18Bitcoin Signed Message:\n', + minFee: BigInt.from(1), // TODO [prio=high]. + minOutput: dustLimit.raw, // TODO. + feePerKb: BigInt.from(1), // TODO. ); // case CryptoCurrencyNetwork.test: // TODO: [prio=low] Add testnet. @@ -159,4 +165,12 @@ class Particl extends Bip39HDCurrency { return false; } } + + @override + bool operator ==(Object other) { + return other is Particl && other.network == network; + } + + @override + int get hashCode => Object.hash(Particl, network); } diff --git a/lib/wallets/crypto_currency/coins/peercoin.dart b/lib/wallets/crypto_currency/coins/peercoin.dart new file mode 100644 index 000000000..4e54329de --- /dev/null +++ b/lib/wallets/crypto_currency/coins/peercoin.dart @@ -0,0 +1,171 @@ +import 'package:coinlib/src/network.dart'; +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; + +class Peercoin extends Bip39HDCurrency { + Peercoin(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.peercoin; + case CryptoCurrencyNetwork.test: + coin = Coin.peercoinTestNet; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 1; + + @override + bool get torSupport => true; + + @override + String constructDerivePath({ + required DerivePathType derivePathType, + int account = 0, + required int chain, + required int index, + }) { + String coinType; + switch (networkParams.wifPrefix) { + case 183: // PPC mainnet wif. + coinType = + "6"; // according to https://github.com/satoshilabs/slips/blob/master/slip-0044.md + break; + case 239: // PPC testnet wif. + coinType = "1"; + break; + default: + throw Exception("Invalid Peercoin network wif used!"); + } + + int purpose; + switch (derivePathType) { + case DerivePathType.bip44: + purpose = 44; + break; + case DerivePathType.bip84: + purpose = 84; + break; + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + + return "m/$purpose'/$coinType'/$account'/$chain/$index"; + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return DefaultNodes.peercoin; + case CryptoCurrencyNetwork.test: + return DefaultNodes.peercoinTestNet; + default: + throw UnimplementedError(); + } + } + + @override + Amount get dustLimit => Amount( + rawValue: BigInt.from(294), + fractionDigits: Coin.peercoin.decimals, + ); + + @override + String get genesisHash { + switch (network) { + case CryptoCurrencyNetwork.main: + return "0000000032fe677166d54963b62a4677d8957e87c508eaa4fd7eb1c880cd27e3"; + case CryptoCurrencyNetwork.test: + return "00000001f757bb737f6596503e17cd17b0658ce630cc727c0cca81aec47c9f06"; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + ({coinlib.Address address, AddressType addressType}) getAddressForPublicKey( + {required coinlib.ECPublicKey publicKey, + required DerivePathType derivePathType}) { + switch (derivePathType) { + // case DerivePathType.bip16: + + case DerivePathType.bip44: + final addr = coinlib.P2PKHAddress.fromPublicKey( + publicKey, + version: networkParams.p2pkhPrefix, + ); + + return (address: addr, addressType: AddressType.p2pkh); + + case DerivePathType.bip49: + final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ).program.script; + + final addr = coinlib.P2SHAddress.fromRedeemScript( + p2wpkhScript, + version: networkParams.p2shPrefix, + ); + + return (address: addr, addressType: AddressType.p2sh); + + case DerivePathType.bip84: + final addr = coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ); + + return (address: addr, addressType: AddressType.p2wpkh); + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + } + + @override + coinlib.Network get networkParams { + switch (network) { + case CryptoCurrencyNetwork.main: + return Network.mainnet; + case CryptoCurrencyNetwork.test: + return Network.testnet; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + List get supportedDerivationPathTypes => [ + DerivePathType.bip44, + DerivePathType.bip84, + ]; + + @override + bool validateAddress(String address) { + try { + coinlib.Address.fromString(address, networkParams); + return true; + } catch (_) { + return false; + } + } + + @override + bool operator ==(Object other) { + return other is Peercoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Peercoin, network); +} diff --git a/lib/wallets/crypto_currency/coins/solana.dart b/lib/wallets/crypto_currency/coins/solana.dart new file mode 100644 index 000000000..6b01b5b6d --- /dev/null +++ b/lib/wallets/crypto_currency/coins/solana.dart @@ -0,0 +1,61 @@ +import 'package:solana/solana.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart'; + +class Solana extends Bip39Currency { + Solana(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.solana; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return NodeModel( + host: + "https://api.mainnet-beta.solana.com/", // TODO: Change this to stack wallet one + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.solana), + useSSL: true, + enabled: true, + coinName: Coin.solana.name, + isFailover: true, + isDown: false, + ); + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 21; + + @override + bool get torSupport => true; + + @override + bool validateAddress(String address) { + return isPointOnEd25519Curve( + Ed25519HDPublicKey.fromBase58(address).toByteArray()); + } + + @override + String get genesisHash => throw UnimplementedError(); + + @override + bool operator ==(Object other) { + return other is Solana && other.network == network; + } + + @override + int get hashCode => Object.hash(Solana, network); +} diff --git a/lib/wallets/crypto_currency/coins/stellar.dart b/lib/wallets/crypto_currency/coins/stellar.dart index c7bbc3d0a..e9a7e605b 100644 --- a/lib/wallets/crypto_currency/coins/stellar.dart +++ b/lib/wallets/crypto_currency/coins/stellar.dart @@ -19,6 +19,9 @@ class Stellar extends Bip39Currency { @override int get minConfirms => 1; + @override + bool get torSupport => true; + @override String get genesisHash => throw UnimplementedError( "Not used for stellar", @@ -39,4 +42,12 @@ class Stellar extends Bip39Currency { @override bool validateAddress(String address) => RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address); + + @override + bool operator ==(Object other) { + return other is Stellar && other.network == network; + } + + @override + int get hashCode => Object.hash(Stellar, network); } diff --git a/lib/wallets/crypto_currency/coins/tezos.dart b/lib/wallets/crypto_currency/coins/tezos.dart index 88a7dadd0..efb982867 100644 --- a/lib/wallets/crypto_currency/coins/tezos.dart +++ b/lib/wallets/crypto_currency/coins/tezos.dart @@ -70,6 +70,9 @@ class Tezos extends Bip39Currency { @override int get minConfirms => 1; + @override + bool get torSupport => true; + @override bool validateAddress(String address) { return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address); @@ -146,4 +149,12 @@ class Tezos extends Bip39Currency { } // =========================================================================== + + @override + bool operator ==(Object other) { + return other is Tezos && other.network == network; + } + + @override + int get hashCode => Object.hash(Tezos, network); } diff --git a/lib/wallets/crypto_currency/coins/wownero.dart b/lib/wallets/crypto_currency/coins/wownero.dart index e96660c00..549d1739f 100644 --- a/lib/wallets/crypto_currency/coins/wownero.dart +++ b/lib/wallets/crypto_currency/coins/wownero.dart @@ -44,4 +44,12 @@ class Wownero extends CryptonoteCurrency { throw UnimplementedError(); } } + + @override + bool operator ==(Object other) { + return other is Wownero && other.network == network; + } + + @override + int get hashCode => Object.hash(Wownero, network); } diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 088e83317..8b6f5cdb6 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -20,6 +20,9 @@ abstract class CryptoCurrency { // (used for eth currently) bool get hasTokenSupport => false; + // Override in subclass if the currency has Tor support: + bool get torSupport => false; + // TODO: [prio=low] require these be overridden in concrete implementations to remove reliance on [coin] int get fractionDigits => coin.decimals; BigInt get satsPerCoin => Constants.satsPerCoin(coin); diff --git a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart index a2899d149..62be12f20 100644 --- a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart @@ -1,5 +1,3 @@ -import 'package:bech32/bech32.dart'; -import 'package:bs58check/bs58check.dart' as bs58check; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; @@ -11,7 +9,7 @@ import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency. abstract class Bip39HDCurrency extends Bip39Currency { Bip39HDCurrency(super.network); - coinlib.NetworkParams get networkParams; + coinlib.Network get networkParams; Amount get dustLimit; @@ -57,37 +55,19 @@ abstract class Bip39HDCurrency extends Bip39Currency { } DerivePathType addressType({required String address}) { - Uint8List? decodeBase58; - Segwit? decodeBech32; - try { - decodeBase58 = bs58check.decode(address); - } catch (err) { - // Base58check decode fail - } - if (decodeBase58 != null) { - if (decodeBase58[0] == networkParams.p2pkhPrefix) { - // P2PKH - return DerivePathType.bip44; - } - if (decodeBase58[0] == networkParams.p2shPrefix) { - // P2SH - return DerivePathType.bip49; - } - throw ArgumentError('Invalid version or Network mismatch'); - } else { - try { - decodeBech32 = segwit.decode(address, networkParams.bech32Hrp); - } catch (err) { - // Bech32 decode fail - } - if (networkParams.bech32Hrp != decodeBech32!.hrp) { - throw ArgumentError('Invalid prefix or Network mismatch'); - } - if (decodeBech32.version != 0) { - throw ArgumentError('Invalid address version'); - } - // P2WPKH + final address2 = coinlib.Address.fromString(address, networkParams); + + if (address2 is coinlib.P2PKHAddress) { + return DerivePathType.bip44; + } else if (address2 is coinlib.P2SHAddress) { + return DerivePathType.bip49; + } else if (address2 is coinlib.P2WPKHAddress) { return DerivePathType.bip84; + } else if (address2 is coinlib.P2TRAddress) { + return DerivePathType.bip86; + } else { + // TODO: [prio=med] better error handling + throw ArgumentError('Invalid address'); } } } diff --git a/lib/wallets/crypto_currency/intermediate/frost_currency.dart b/lib/wallets/crypto_currency/intermediate/frost_currency.dart new file mode 100644 index 000000000..0c10937fa --- /dev/null +++ b/lib/wallets/crypto_currency/intermediate/frost_currency.dart @@ -0,0 +1,12 @@ +import 'dart:typed_data'; + +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; + +abstract class FrostCurrency extends CryptoCurrency { + FrostCurrency(super.network); + + String pubKeyToScriptHash({required Uint8List pubKey}); + + Amount get dustLimit; +} diff --git a/lib/wallets/isar/models/frost_wallet_info.dart b/lib/wallets/isar/models/frost_wallet_info.dart new file mode 100644 index 000000000..b5c7476d2 --- /dev/null +++ b/lib/wallets/isar/models/frost_wallet_info.dart @@ -0,0 +1,41 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/wallets/isar/isar_id_interface.dart'; + +part 'frost_wallet_info.g.dart'; + +@Collection(accessor: "frostWalletInfo", inheritance: false) +class FrostWalletInfo implements IsarId { + @override + Id id = Isar.autoIncrement; + + @Index(unique: true, replace: false) + final String walletId; + + final List knownSalts; + final List participants; + final String myName; + final int threshold; + + FrostWalletInfo({ + required this.walletId, + required this.knownSalts, + required this.participants, + required this.myName, + required this.threshold, + }); + + FrostWalletInfo copyWith({ + List? knownSalts, + List? participants, + String? myName, + int? threshold, + }) { + return FrostWalletInfo( + walletId: walletId, + knownSalts: knownSalts ?? this.knownSalts, + participants: participants ?? this.participants, + myName: myName ?? this.myName, + threshold: threshold ?? this.threshold, + )..id = id; + } +} diff --git a/lib/wallets/isar/models/frost_wallet_info.g.dart b/lib/wallets/isar/models/frost_wallet_info.g.dart new file mode 100644 index 000000000..6c80125e2 --- /dev/null +++ b/lib/wallets/isar/models/frost_wallet_info.g.dart @@ -0,0 +1,1364 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'frost_wallet_info.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters + +extension GetFrostWalletInfoCollection on Isar { + IsarCollection get frostWalletInfo => this.collection(); +} + +const FrostWalletInfoSchema = CollectionSchema( + name: r'FrostWalletInfo', + id: -4182879703273806681, + properties: { + r'knownSalts': PropertySchema( + id: 0, + name: r'knownSalts', + type: IsarType.stringList, + ), + r'myName': PropertySchema( + id: 1, + name: r'myName', + type: IsarType.string, + ), + r'participants': PropertySchema( + id: 2, + name: r'participants', + type: IsarType.stringList, + ), + r'threshold': PropertySchema( + id: 3, + name: r'threshold', + type: IsarType.long, + ), + r'walletId': PropertySchema( + id: 4, + name: r'walletId', + type: IsarType.string, + ) + }, + estimateSize: _frostWalletInfoEstimateSize, + serialize: _frostWalletInfoSerialize, + deserialize: _frostWalletInfoDeserialize, + deserializeProp: _frostWalletInfoDeserializeProp, + idName: r'id', + indexes: { + r'walletId': IndexSchema( + id: -1783113319798776304, + name: r'walletId', + unique: true, + replace: false, + properties: [ + IndexPropertySchema( + name: r'walletId', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _frostWalletInfoGetId, + getLinks: _frostWalletInfoGetLinks, + attach: _frostWalletInfoAttach, + version: '3.0.5', +); + +int _frostWalletInfoEstimateSize( + FrostWalletInfo object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.knownSalts.length * 3; + { + for (var i = 0; i < object.knownSalts.length; i++) { + final value = object.knownSalts[i]; + bytesCount += value.length * 3; + } + } + bytesCount += 3 + object.myName.length * 3; + bytesCount += 3 + object.participants.length * 3; + { + for (var i = 0; i < object.participants.length; i++) { + final value = object.participants[i]; + bytesCount += value.length * 3; + } + } + bytesCount += 3 + object.walletId.length * 3; + return bytesCount; +} + +void _frostWalletInfoSerialize( + FrostWalletInfo object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeStringList(offsets[0], object.knownSalts); + writer.writeString(offsets[1], object.myName); + writer.writeStringList(offsets[2], object.participants); + writer.writeLong(offsets[3], object.threshold); + writer.writeString(offsets[4], object.walletId); +} + +FrostWalletInfo _frostWalletInfoDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = FrostWalletInfo( + knownSalts: reader.readStringList(offsets[0]) ?? [], + myName: reader.readString(offsets[1]), + participants: reader.readStringList(offsets[2]) ?? [], + threshold: reader.readLong(offsets[3]), + walletId: reader.readString(offsets[4]), + ); + object.id = id; + return object; +} + +P _frostWalletInfoDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringList(offset) ?? []) as P; + case 1: + return (reader.readString(offset)) as P; + case 2: + return (reader.readStringList(offset) ?? []) as P; + case 3: + return (reader.readLong(offset)) as P; + case 4: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _frostWalletInfoGetId(FrostWalletInfo object) { + return object.id; +} + +List> _frostWalletInfoGetLinks(FrostWalletInfo object) { + return []; +} + +void _frostWalletInfoAttach( + IsarCollection col, Id id, FrostWalletInfo object) { + object.id = id; +} + +extension FrostWalletInfoByIndex on IsarCollection { + Future getByWalletId(String walletId) { + return getByIndex(r'walletId', [walletId]); + } + + FrostWalletInfo? getByWalletIdSync(String walletId) { + return getByIndexSync(r'walletId', [walletId]); + } + + Future deleteByWalletId(String walletId) { + return deleteByIndex(r'walletId', [walletId]); + } + + bool deleteByWalletIdSync(String walletId) { + return deleteByIndexSync(r'walletId', [walletId]); + } + + Future> getAllByWalletId(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return getAllByIndex(r'walletId', values); + } + + List getAllByWalletIdSync(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'walletId', values); + } + + Future deleteAllByWalletId(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'walletId', values); + } + + int deleteAllByWalletIdSync(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'walletId', values); + } + + Future putByWalletId(FrostWalletInfo object) { + return putByIndex(r'walletId', object); + } + + Id putByWalletIdSync(FrostWalletInfo object, {bool saveLinks = true}) { + return putByIndexSync(r'walletId', object, saveLinks: saveLinks); + } + + Future> putAllByWalletId(List objects) { + return putAllByIndex(r'walletId', objects); + } + + List putAllByWalletIdSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'walletId', objects, saveLinks: saveLinks); + } +} + +extension FrostWalletInfoQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension FrostWalletInfoQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + walletIdEqualTo(String walletId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'walletId', + value: [walletId], + )); + }); + } + + QueryBuilder + walletIdNotEqualTo(String walletId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [], + upper: [walletId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [walletId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [walletId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [], + upper: [walletId], + includeUpper: false, + )); + } + }); + } +} + +extension FrostWalletInfoQueryFilter + on QueryBuilder { + QueryBuilder + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + knownSaltsElementEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'knownSalts', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'knownSalts', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'knownSalts', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + knownSaltsElementIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'knownSalts', + value: '', + )); + }); + } + + QueryBuilder + knownSaltsElementIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'knownSalts', + value: '', + )); + }); + } + + QueryBuilder + knownSaltsLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + knownSaltsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + knownSaltsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + knownSaltsLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + knownSaltsLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + knownSaltsLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'knownSalts', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder + myNameEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'myName', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'myName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'myName', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + myNameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'myName', + value: '', + )); + }); + } + + QueryBuilder + myNameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'myName', + value: '', + )); + }); + } + + QueryBuilder + participantsElementEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'participants', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'participants', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'participants', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + participantsElementIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'participants', + value: '', + )); + }); + } + + QueryBuilder + participantsElementIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'participants', + value: '', + )); + }); + } + + QueryBuilder + participantsLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + participantsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + participantsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + participantsLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + participantsLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + participantsLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'participants', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder + thresholdEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'threshold', + value: value, + )); + }); + } + + QueryBuilder + thresholdGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'threshold', + value: value, + )); + }); + } + + QueryBuilder + thresholdLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'threshold', + value: value, + )); + }); + } + + QueryBuilder + thresholdBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'threshold', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + walletIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'walletId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'walletId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: '', + )); + }); + } + + QueryBuilder + walletIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'walletId', + value: '', + )); + }); + } +} + +extension FrostWalletInfoQueryObject + on QueryBuilder {} + +extension FrostWalletInfoQueryLinks + on QueryBuilder {} + +extension FrostWalletInfoQuerySortBy + on QueryBuilder { + QueryBuilder sortByMyName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'myName', Sort.asc); + }); + } + + QueryBuilder + sortByMyNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'myName', Sort.desc); + }); + } + + QueryBuilder + sortByThreshold() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'threshold', Sort.asc); + }); + } + + QueryBuilder + sortByThresholdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'threshold', Sort.desc); + }); + } + + QueryBuilder + sortByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder + sortByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension FrostWalletInfoQuerySortThenBy + on QueryBuilder { + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByMyName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'myName', Sort.asc); + }); + } + + QueryBuilder + thenByMyNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'myName', Sort.desc); + }); + } + + QueryBuilder + thenByThreshold() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'threshold', Sort.asc); + }); + } + + QueryBuilder + thenByThresholdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'threshold', Sort.desc); + }); + } + + QueryBuilder + thenByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder + thenByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension FrostWalletInfoQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByKnownSalts() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'knownSalts'); + }); + } + + QueryBuilder distinctByMyName( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'myName', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByParticipants() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'participants'); + }); + } + + QueryBuilder + distinctByThreshold() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'threshold'); + }); + } + + QueryBuilder distinctByWalletId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); + }); + } +} + +extension FrostWalletInfoQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder, QQueryOperations> + knownSaltsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'knownSalts'); + }); + } + + QueryBuilder myNameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'myName'); + }); + } + + QueryBuilder, QQueryOperations> + participantsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'participants'); + }); + } + + QueryBuilder thresholdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'threshold'); + }); + } + + QueryBuilder walletIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'walletId'); + }); + } +} diff --git a/lib/wallets/isar/models/wallet_info.g.dart b/lib/wallets/isar/models/wallet_info.g.dart index db50a581a..bc0a5828e 100644 --- a/lib/wallets/isar/models/wallet_info.g.dart +++ b/lib/wallets/isar/models/wallet_info.g.dart @@ -265,6 +265,9 @@ const _WalletInfomainAddressTypeEnumValueMap = { 'spark': 10, 'stellar': 11, 'tezos': 12, + 'frostMS': 13, + 'p2tr': 14, + 'solana': 15, }; const _WalletInfomainAddressTypeValueEnumMap = { 0: AddressType.p2pkh, @@ -280,6 +283,9 @@ const _WalletInfomainAddressTypeValueEnumMap = { 10: AddressType.spark, 11: AddressType.stellar, 12: AddressType.tezos, + 13: AddressType.frostMS, + 14: AddressType.p2tr, + 15: AddressType.solana, }; Id _walletInfoGetId(WalletInfo object) { diff --git a/lib/wallets/models/incomplete_frost_wallet.dart b/lib/wallets/models/incomplete_frost_wallet.dart new file mode 100644 index 000000000..015ca9fb9 --- /dev/null +++ b/lib/wallets/models/incomplete_frost_wallet.dart @@ -0,0 +1,44 @@ +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; + +class IncompleteFrostWallet { + WalletInfo? info; + + String? get walletId => info?.walletId; + + Future toBitcoinFrostWallet({ + required MainDB mainDB, + required SecureStorageInterface secureStorageInterface, + required NodeService nodeService, + required Prefs prefs, + }) async { + final wallet = await Wallet.create( + walletInfo: info!, + mainDB: mainDB, + secureStorageInterface: secureStorageInterface, + nodeService: nodeService, + prefs: prefs, + ); + + // dummy entry so updaters work when `wallet.updateWithResharedData` is called + final frostInfo = FrostWalletInfo( + walletId: info!.walletId, + knownSalts: [], + participants: [], + myName: "", + threshold: -1, + ); + + await mainDB.isar.writeTxn(() async { + await mainDB.isar.frostWalletInfo.put(frostInfo); + }); + + return wallet as BitcoinFrostWallet; + } +} diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 22101003c..d98dd088e 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -33,7 +33,9 @@ class TxData { final String? changeAddress; + // frost specific final String? frostMSConfig; + final List? frostSigners; // paynym specific final PaynymAccountLite? paynymAccountLite; @@ -91,6 +93,7 @@ class TxData { this.usedUTXOs, this.changeAddress, this.frostMSConfig, + this.frostSigners, this.paynymAccountLite, this.web3dartTransaction, this.nonce, @@ -166,6 +169,7 @@ class TxData { })>? recipients, String? frostMSConfig, + List? frostSigners, String? changeAddress, PaynymAccountLite? paynymAccountLite, web3dart.Transaction? web3dartTransaction, @@ -209,6 +213,7 @@ class TxData { usedUTXOs: usedUTXOs ?? this.usedUTXOs, recipients: recipients ?? this.recipients, frostMSConfig: frostMSConfig ?? this.frostMSConfig, + frostSigners: frostSigners ?? this.frostSigners, changeAddress: changeAddress ?? this.changeAddress, paynymAccountLite: paynymAccountLite ?? this.paynymAccountLite, web3dartTransaction: web3dartTransaction ?? this.web3dartTransaction, @@ -249,7 +254,7 @@ class TxData { 'recipients: $recipients, ' 'utxos: $utxos, ' 'usedUTXOs: $usedUTXOs, ' - 'frostMSConfig: $frostMSConfig, ' + 'frostSigners: $frostSigners, ' 'changeAddress: $changeAddress, ' 'paynymAccountLite: $paynymAccountLite, ' 'web3dartTransaction: $web3dartTransaction, ' diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart new file mode 100644 index 000000000..5d0bb3ffc --- /dev/null +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -0,0 +1,1176 @@ +import 'dart:async'; +import 'dart:ffi'; + +import 'package:flutter/foundation.dart'; +import 'package:frostdart/frostdart.dart' as frost; +import 'package:frostdart/frostdart_bindings_generated.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/frost.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/frost_currency.dart'; +import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; + +class BitcoinFrostWallet extends Wallet { + BitcoinFrostWallet(CryptoCurrencyNetwork network) + : super(BitcoinFrost(network) as T); + + FrostWalletInfo get frostInfo => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .findFirstSync()!; + + late ElectrumXClient electrumXClient; + late CachedElectrumXClient electrumXCachedClient; + + Future initializeNewFrost({ + required String multisigConfig, + required String recoveryString, + required String serializedKeys, + required Uint8List multisigId, + required String myName, + required List participants, + required int threshold, + }) async { + Logging.instance.log( + "Generating new FROST wallet.", + level: LogLevel.Info, + ); + + try { + final salt = frost + .multisigSalt( + multisigConfig: multisigConfig, + ) + .toHex; + + final FrostWalletInfo frostWalletInfo = FrostWalletInfo( + walletId: info.walletId, + knownSalts: [salt], + participants: participants, + myName: myName, + threshold: threshold, + ); + + await _saveSerializedKeys(serializedKeys); + await _saveRecoveryString(recoveryString); + await _saveMultisigId(multisigId); + await _saveMultisigConfig(multisigConfig); + + await mainDB.isar.writeTxn(() async { + await mainDB.isar.frostWalletInfo.put(frostWalletInfo); + }); + + final keys = frost.deserializeKeys(keys: serializedKeys); + + final addressString = frost.addressForKeys( + network: cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet, + keys: keys, + ); + + final publicKey = frost.scriptPubKeyForKeys(keys: keys); + + final address = Address( + walletId: info.walletId, + value: addressString, + publicKey: publicKey.toUint8ListFromHex, + derivationIndex: 0, + derivationPath: null, + subType: AddressSubType.receiving, + type: AddressType.unknown, + ); + + await mainDB.putAddresses([address]); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from initializeNewFrost(): $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } + + Future frostCreateSignConfig({ + required TxData txData, + required String changeAddress, + required int feePerWeight, + }) async { + try { + if (txData.recipients == null || txData.recipients!.isEmpty) { + throw Exception("No recipients found!"); + } + + final total = txData.recipients! + .map((e) => e.amount) + .reduce((value, e) => value += e); + + final utxos = await mainDB + .getUTXOs(walletId) + .filter() + .isBlockedEqualTo(false) + .findAll(); + + if (utxos.isEmpty) { + throw Exception("No UTXOs found"); + } else { + final currentHeight = await chainHeight; + utxos.removeWhere( + (e) => !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + ), + ); + if (utxos.isEmpty) { + throw Exception("No confirmed UTXOs found"); + } + } + + if (total.raw > + utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e)) { + throw Exception("Insufficient available funds"); + } + + Amount sum = Amount.zeroWith( + fractionDigits: cryptoCurrency.fractionDigits, + ); + final Set utxosToUse = {}; + for (final utxo in utxos) { + sum += Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: cryptoCurrency.fractionDigits, + ); + utxosToUse.add(utxo); + if (sum > total) { + break; + } + } + + final serializedKeys = await getSerializedKeys(); + final keys = frost.deserializeKeys(keys: serializedKeys!); + + final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet; + + final publicKey = frost + .scriptPubKeyForKeys( + keys: keys, + ) + .toUint8ListFromHex; + + final config = Frost.createSignConfig( + network: network, + inputs: utxosToUse + .map( + (e) => ( + utxo: e, + scriptPubKey: publicKey, + ), + ) + .toList(), + outputs: txData.recipients!, + changeAddress: (await getCurrentReceivingAddress())!.value, + feePerWeight: feePerWeight, + ); + + return txData.copyWith(frostMSConfig: config, utxos: utxosToUse); + } catch (_) { + rethrow; + } + } + + Future< + ({ + Pointer machinePtr, + String preprocess, + })> frostAttemptSignConfig({ + required String config, + }) async { + final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet; + final serializedKeys = await getSerializedKeys(); + + return Frost.attemptSignConfig( + network: network, + config: config, + serializedKeys: serializedKeys!, + ); + } + + Future updateWithResharedData({ + required String serializedKeys, + required String multisigConfig, + required bool isNewWallet, + }) async { + await _saveSerializedKeys(serializedKeys); + await _saveMultisigConfig(multisigConfig); + + await _updateThreshold( + frost.getThresholdFromKeys( + serializedKeys: serializedKeys, + ), + ); + + final myNameIndex = frost.getParticipantIndexFromKeys( + serializedKeys: serializedKeys, + ); + final participants = Frost.getParticipants( + multisigConfig: multisigConfig, + ); + final myName = participants[myNameIndex]; + + await _updateParticipants(participants); + await _updateMyName(myName); + + if (isNewWallet) { + await recover( + serializedKeys: serializedKeys, + multisigConfig: multisigConfig, + isRescan: false, + ); + } + } + + Future sweepAllEstimate(int feeRate) async { + int available = 0; + int inputCount = 0; + final height = await chainHeight; + for (final output in (await mainDB.getUTXOs(walletId).findAll())) { + if (!output.isBlocked && + output.isConfirmed(height, cryptoCurrency.minConfirms)) { + available += output.value; + inputCount++; + } + } + + // transaction will only have 1 output minus the fee + final estimatedFee = _roughFeeEstimate(inputCount, 1, feeRate); + + return Amount( + rawValue: BigInt.from(available), + fractionDigits: cryptoCurrency.fractionDigits, + ) - + estimatedFee; + } + + // int _estimateTxFee({required int vSize, required int feeRatePerKB}) { + // return vSize * (feeRatePerKB / 1000).ceil(); + // } + + Amount _roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil(), + ), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + // ==================== Overrides ============================================ + + @override + bool get supportsMultiRecipient => true; + + @override + int get isarTransactionVersion => 2; + + @override + FilterOperation? get changeAddressFilterOperation => FilterGroup.and( + [ + FilterCondition.equalTo( + property: r"type", + value: info.mainAddressType, + ), + const FilterCondition.equalTo( + property: r"subType", + value: AddressSubType.change, + ), + ], + ); + + @override + FilterOperation? get receivingAddressFilterOperation => FilterGroup.and( + [ + FilterCondition.equalTo( + property: r"type", + value: info.mainAddressType, + ), + const FilterCondition.equalTo( + property: r"subType", + value: AddressSubType.receiving, + ), + ], + ); + + @override + Future updateTransactions() async { + final myAddress = (await getCurrentReceivingAddress())!; + + final scriptHash = cryptoCurrency.pubKeyToScriptHash( + pubKey: Uint8List.fromList(myAddress.publicKey), + ); + final allTxHashes = + (await electrumXClient.getHistory(scripthash: scriptHash)).toSet(); + + final currentHeight = await chainHeight; + final coin = info.coin; + + final List> allTransactions = []; + + for (final txHash in allTxHashes) { + final storedTx = await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(txHash["tx_hash"] as String) + .findFirst(); + + if (storedTx == null || + !storedTx.isConfirmed(currentHeight, cryptoCurrency.minConfirms)) { + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: coin, + ); + + if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + } + + // Parse all new txs. + final List txns = []; + for (final txData in allTransactions) { + bool wasSentFromThisWallet = false; + // Set to true if any inputs were detected as owned by this wallet. + + bool wasReceivedInThisWallet = false; + // Set to true if any outputs were detected as owned by this wallet. + + // Parse inputs. + BigInt amountReceivedInThisWallet = BigInt.zero; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + if (coinbase == null) { + // Not a coinbase (ie a typical input). + final txid = map["txid"] as String; + final vout = map["vout"] as int; + + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + coin: cryptoCurrency.coin, + ); + + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map, + ); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + walletOwns: false, // Doesn't matter here as this is not saved. + ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } + + InputV2 input = InputV2.fromElectrumxJson( + json: map, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + coinbase: coinbase, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // Check if input was from this wallet. + if (input.addresses.contains(myAddress.value)) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + // Parse outputs. + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // If output was to my wallet, add value to amount received. + if (output.addresses.contains(myAddress.value)) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + + outputs.add(output); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + TransactionSubType subType = TransactionSubType.none; + if (outputs.length > 1 && inputs.isNotEmpty) { + for (int i = 0; i < outputs.length; i++) { + final List? scriptChunks = + outputs[i].scriptPubKeyAsm?.split(" "); + if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { + final blindedPaymentCode = scriptChunks![1]; + final bytes = blindedPaymentCode.toUint8ListFromHex; + + // https://en.bitcoin.it/wiki/BIP_0047#Sending + if (bytes.length == 80 && bytes.first == 1) { + subType = TransactionSubType.bip47Notification; + break; + } + } + } + } + + // At least one input was owned by this wallet. + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (amountReceivedInThisWallet == totalOut) { + // Definitely sent all to self. + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // Most likely just a typical send, do nothing here yet. + } + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.log( + "Unexpected tx found (ignoring it): $txData", + level: LogLevel.Error, + ); + continue; + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: null, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } + + @override + Future checkSaveInitialReceivingAddress() async { + final address = await getCurrentReceivingAddress(); + if (address == null) { + final serializedKeys = await getSerializedKeys(); + if (serializedKeys != null) { + final keys = frost.deserializeKeys(keys: serializedKeys); + + final addressString = frost.addressForKeys( + network: cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet, + keys: keys, + ); + + final publicKey = frost.scriptPubKeyForKeys(keys: keys); + + final address = Address( + walletId: walletId, + value: addressString, + publicKey: publicKey.toUint8ListFromHex, + derivationIndex: 0, + derivationPath: null, + subType: AddressSubType.receiving, + type: AddressType.frostMS, + ); + + await mainDB.updateOrPutAddresses([address]); + } else { + Logging.instance.log( + "$runtimeType.checkSaveInitialReceivingAddress() failed due" + " to missing serialized keys", + level: LogLevel.Fatal, + ); + } + } + } + + @override + Future confirmSend({required TxData txData}) async { + try { + Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); + + final hex = txData.raw!; + + final txHash = await electrumXClient.broadcastTransaction(rawTx: hex); + Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); + + // mark utxos as used + final usedUTXOs = txData.utxos!.map((e) => e.copyWith(used: true)); + await mainDB.putUTXOs(usedUTXOs.toList()); + + txData = txData.copyWith( + utxos: usedUTXOs.toSet(), + txHash: txHash, + txid: txHash, + ); + + return txData; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + @override + Future estimateFeeFor(Amount amount, int feeRate) async { + final available = info.cachedBalance.spendable; + + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { + return _roughFeeEstimate(1, 2, feeRate); + } + + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + int inputCount = 0; + for (final output in (await mainDB.getUTXOs(walletId).findAll())) { + if (!output.isBlocked) { + runningBalance += Amount( + rawValue: BigInt.from(output.value), + fractionDigits: cryptoCurrency.fractionDigits, + ); + inputCount++; + if (runningBalance > amount) { + break; + } + } + } + + final oneOutPutFee = _roughFeeEstimate(inputCount, 1, feeRate); + final twoOutPutFee = _roughFeeEstimate(inputCount, 2, feeRate); + + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + cryptoCurrency.dustLimit) { + final change = runningBalance - amount - twoOutPutFee; + if (change > cryptoCurrency.dustLimit && + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; + } else { + return runningBalance - amount; + } + } else { + return runningBalance - amount; + } + } else if (runningBalance - amount == oneOutPutFee) { + return oneOutPutFee; + } else { + return twoOutPutFee; + } + } + + @override + Future get fees async { + try { + // adjust numbers for different speeds? + const int f = 1, m = 5, s = 20; + + final fast = await electrumXClient.estimateFee(blocks: f); + final medium = await electrumXClient.estimateFee(blocks: m); + final slow = await electrumXClient.estimateFee(blocks: s); + + final feeObject = FeeObject( + numberOfBlocksFast: f, + numberOfBlocksAverage: m, + numberOfBlocksSlow: s, + fast: Amount.fromDecimal( + fast, + fractionDigits: cryptoCurrency.fractionDigits, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: cryptoCurrency.fractionDigits, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: cryptoCurrency.fractionDigits, + ).raw.toInt(), + ); + + Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); + return feeObject; + } catch (e) { + Logging.instance + .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); + rethrow; + } + } + + @override + Future prepareSend({required TxData txData}) { + // TODO: implement prepareSendpu + throw UnimplementedError(); + } + + @override + Future recover({ + required bool isRescan, + String? serializedKeys, + String? multisigConfig, + }) async { + if (serializedKeys == null || multisigConfig == null) { + serializedKeys = await getSerializedKeys(); + multisigConfig = await getMultisigConfig(); + } + if (serializedKeys == null || multisigConfig == null) { + final err = "${info.coinName} wallet ${info.walletId} had null keys/cfg"; + Logging.instance.log(err, level: LogLevel.Fatal); + throw Exception(err); + // TODO [prio=low]: handle null keys or config. This should not happen. + } + + final coin = info.coin; + + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + try { + await refreshMutex.protect(() async { + if (!isRescan) { + final salt = frost + .multisigSalt( + multisigConfig: multisigConfig!, + ) + .toHex; + final knownSalts = _getKnownSalts(); + if (knownSalts.contains(salt)) { + throw Exception("Known frost multisig salt found!"); + } + final List updatedKnownSalts = List.from(knownSalts); + updatedKnownSalts.add(salt); + await _updateKnownSalts(updatedKnownSalts); + } else { + // clear cache + await electrumXCachedClient.clearSharedTransactionCache(coin: coin); + await mainDB.deleteWalletBlockchainData(walletId); + } + + final keys = frost.deserializeKeys(keys: serializedKeys!); + await _saveSerializedKeys(serializedKeys!); + await _saveMultisigConfig(multisigConfig!); + + final addressString = frost.addressForKeys( + network: cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet, + keys: keys, + ); + + final publicKey = frost.scriptPubKeyForKeys(keys: keys); + + final address = Address( + walletId: walletId, + value: addressString, + publicKey: publicKey.toUint8ListFromHex, + derivationIndex: 0, + derivationPath: null, + subType: AddressSubType.receiving, + type: AddressType.frostMS, + ); + + await mainDB.updateOrPutAddresses([address]); + }); + + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + + unawaited(refresh()); + } catch (e, s) { + Logging.instance.log( + "recoverFromSerializedKeys failed: $e\n$s", + level: LogLevel.Fatal, + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + rethrow; + } + } + + @override + Future updateBalance() async { + final utxos = await mainDB.getUTXOs(walletId).findAll(); + + final currentChainHeight = await chainHeight; + + Amount satoshiBalanceTotal = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalancePending = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalanceSpendable = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalanceBlocked = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + for (final utxo in utxos) { + final utxoAmount = Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + satoshiBalanceTotal += utxoAmount; + + if (utxo.isBlocked) { + satoshiBalanceBlocked += utxoAmount; + } else { + if (utxo.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + )) { + satoshiBalanceSpendable += utxoAmount; + } else { + satoshiBalancePending += utxoAmount; + } + } + } + + final balance = Balance( + total: satoshiBalanceTotal, + spendable: satoshiBalanceSpendable, + blockedTotal: satoshiBalanceBlocked, + pendingSpendable: satoshiBalancePending, + ); + + await info.updateBalance(newBalance: balance, isar: mainDB.isar); + } + + @override + Future updateChainHeight() async { + final int height; + try { + final result = await electrumXClient.getBlockHeadTip(); + height = result["height"] as int; + } catch (e) { + rethrow; + } + + await info.updateCachedChainHeight( + newHeight: height, + isar: mainDB.isar, + ); + } + + @override + Future pingCheck() async { + try { + final result = await electrumXClient.ping(); + return result; + } catch (_) { + return false; + } + } + + @override + Future updateNode() async { + await _updateElectrumX(); + } + + @override + Future updateUTXOs() async { + final address = await getCurrentReceivingAddress(); + + try { + final scriptHash = cryptoCurrency.pubKeyToScriptHash( + pubKey: Uint8List.fromList(address!.publicKey), + ); + + final utxos = await electrumXClient.getUTXOs(scripthash: scriptHash); + + final List outputArray = []; + + for (int i = 0; i < utxos.length; i++) { + final utxo = await _parseUTXO( + jsonUTXO: utxos[i], + ); + + outputArray.add(utxo); + } + + return await mainDB.updateUTXOs(walletId, outputArray); + } catch (e, s) { + Logging.instance.log( + "Output fetch unsuccessful: $e\n$s", + level: LogLevel.Error, + ); + return false; + } + } + + // =================== Secure storage ======================================== + + Future getSerializedKeys() async => + await secureStorageInterface.read( + key: "{$walletId}_serializedFROSTKeys", + ); + + Future _saveSerializedKeys( + String keys, + ) async { + final current = await getSerializedKeys(); + + if (current == null) { + // do nothing + } else if (current == keys) { + // should never occur + } else { + // save current as prev gen before updating current + await secureStorageInterface.write( + key: "{$walletId}_serializedFROSTKeysPrevGen", + value: current, + ); + } + + await secureStorageInterface.write( + key: "{$walletId}_serializedFROSTKeys", + value: keys, + ); + } + + Future getSerializedKeysPrevGen() async => + await secureStorageInterface.read( + key: "{$walletId}_serializedFROSTKeysPrevGen", + ); + + Future getMultisigConfig() async => + await secureStorageInterface.read( + key: "{$walletId}_multisigConfig", + ); + + Future getMultisigConfigPrevGen() async => + await secureStorageInterface.read( + key: "{$walletId}_multisigConfigPrevGen", + ); + + Future _saveMultisigConfig( + String multisigConfig, + ) async { + final current = await getMultisigConfig(); + + if (current == null) { + // do nothing + } else if (current == multisigConfig) { + // should never occur + } else { + // save current as prev gen before updating current + await secureStorageInterface.write( + key: "{$walletId}_multisigConfigPrevGen", + value: current, + ); + } + + await secureStorageInterface.write( + key: "{$walletId}_multisigConfig", + value: multisigConfig, + ); + } + + Future _multisigId() async { + final id = await secureStorageInterface.read( + key: "{$walletId}_multisigIdFROST", + ); + if (id == null) { + return null; + } else { + return id.toUint8ListFromHex; + } + } + + Future _saveMultisigId( + Uint8List id, + ) async => + await secureStorageInterface.write( + key: "{$walletId}_multisigIdFROST", + value: id.toHex, + ); + + Future _recoveryString() async => await secureStorageInterface.read( + key: "{$walletId}_recoveryStringFROST", + ); + + Future _saveRecoveryString( + String recoveryString, + ) async => + await secureStorageInterface.write( + key: "{$walletId}_recoveryStringFROST", + value: recoveryString, + ); + + // =================== DB ==================================================== + + List _getKnownSalts() => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .knownSaltsProperty() + .findFirstSync()!; + + Future _updateKnownSalts(List knownSalts) async { + final info = frostInfo; + + await mainDB.isar.writeTxn(() async { + await mainDB.isar.frostWalletInfo.delete(info.id); + await mainDB.isar.frostWalletInfo.put( + info.copyWith(knownSalts: knownSalts), + ); + }); + } + + List _getParticipants() => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .participantsProperty() + .findFirstSync()!; + + Future _updateParticipants(List participants) async { + final info = frostInfo; + + await mainDB.isar.writeTxn(() async { + await mainDB.isar.frostWalletInfo.delete(info.id); + await mainDB.isar.frostWalletInfo.put( + info.copyWith(participants: participants), + ); + }); + } + + int _getThreshold() => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .thresholdProperty() + .findFirstSync()!; + + Future _updateThreshold(int threshold) async { + final info = frostInfo; + + await mainDB.isar.writeTxn(() async { + await mainDB.isar.frostWalletInfo.delete(info.id); + await mainDB.isar.frostWalletInfo.put( + info.copyWith(threshold: threshold), + ); + }); + } + + String _getMyName() => mainDB.isar.frostWalletInfo + .where() + .walletIdEqualTo(walletId) + .myNameProperty() + .findFirstSync()!; + + Future _updateMyName(String myName) async { + final info = frostInfo; + + await mainDB.isar.writeTxn(() async { + await mainDB.isar.frostWalletInfo.delete(info.id); + await mainDB.isar.frostWalletInfo.put( + info.copyWith(myName: myName), + ); + }); + } + + // =================== Private =============================================== + + Future _getCurrentElectrumXNode() async { + final node = getCurrentNode(); + + return ElectrumXNode( + address: node.host, + port: node.port, + name: node.name, + useSSL: node.useSSL, + id: node.id, + ); + } + + // TODO [prio=low]: Use ElectrumXInterface method. + Future _updateElectrumX() async { + final failovers = nodeService + .failoverNodesFor(coin: cryptoCurrency.coin) + .map( + (e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + ), + ) + .toList(); + + final newNode = await _getCurrentElectrumXNode(); + try { + await electrumXClient.closeAdapter(); + } catch (e) { + if (e.toString().contains("initialized")) { + // Ignore. This should happen every first time the wallet is opened. + } else { + Logging.instance.log( + "Error closing electrumXClient: $e", + level: LogLevel.Error, + ); + } + } + electrumXClient = ElectrumXClient.from( + node: newNode, + prefs: prefs, + failovers: failovers, + cryptoCurrency: cryptoCurrency, + ); + + electrumXCachedClient = CachedElectrumXClient.from( + electrumXClient: electrumXClient, + ); + } + + bool _duplicateTxCheck( + List> allTransactions, + String txid, + ) { + for (int i = 0; i < allTransactions.length; i++) { + if (allTransactions[i]["txid"] == txid) { + return true; + } + } + return false; + } + + Future _parseUTXO({ + required Map jsonUTXO, + }) async { + final txn = await electrumXCachedClient.getTransaction( + txHash: jsonUTXO["tx_hash"] as String, + verbose: true, + coin: cryptoCurrency.coin, + ); + + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + // String? scriptPubKey; + String? utxoOwnerAddress; + // get UTXO owner address + for (final output in outputs) { + if (output["n"] == vout) { + // scriptPubKey = output["scriptPubKey"]?["hex"] as String?; + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + } + } + + final utxo = UTXO( + walletId: walletId, + txid: txn["txid"] as String, + vout: vout, + value: jsonUTXO["value"] as int, + name: "", + isBlocked: false, + blockedReason: null, + isCoinbase: txn["is_coinbase"] as bool? ?? false, + blockHash: txn["blockhash"] as String?, + blockHeight: jsonUTXO["height"] as int?, + blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, + ); + + return utxo; + } +} diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 8228ab7c8..b2e5ad3f1 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -36,7 +36,7 @@ import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart'; -import 'package:websocket_universal/websocket_universal.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; // // refactor of https://github.com/cypherstack/stack_wallet/blob/1d9fb4cd069f22492ece690ac788e05b8f8b1209/lib/services/coins/epiccash/epiccash_wallet.dart @@ -50,9 +50,9 @@ class EpiccashWallet extends Bip39Wallet { double highestPercent = 0; Future get getSyncPercent async { - int lastScannedBlock = info.epicData?.lastScannedBlock ?? 0; + final int lastScannedBlock = info.epicData?.lastScannedBlock ?? 0; final _chainHeight = await chainHeight; - double restorePercent = lastScannedBlock / _chainHeight; + final double restorePercent = lastScannedBlock / _chainHeight; GlobalEventBus.instance .fire(RefreshPercentChangedEvent(highestPercent, walletId)); if (restorePercent > highestPercent) { @@ -67,7 +67,7 @@ class EpiccashWallet extends Bip39Wallet { } Future updateEpicboxConfig(String host, int port) async { - String stringConfig = jsonEncode({ + final String stringConfig = jsonEncode({ "epicbox_domain": host, "epicbox_port": port, "epicbox_protocol_unsecure": false, @@ -103,15 +103,16 @@ class EpiccashWallet extends Bip39Wallet { } Future getEpicBoxConfig() async { - EpicBoxConfigModel? _epicBoxConfig = - EpicBoxConfigModel.fromServer(DefaultEpicBoxes.defaultEpicBoxServer); + final EpicBoxConfigModel _epicBoxConfig = EpicBoxConfigModel.fromServer( + DefaultEpicBoxes.defaultEpicBoxServer, + ); //Get the default Epicbox server and check if it's conected // bool isEpicboxConnected = await _testEpicboxServer( // DefaultEpicBoxes.defaultEpicBoxServer.host, DefaultEpicBoxes.defaultEpicBoxServer.port ?? 443); // if (isEpicboxConnected) { - //Use default server for as Epicbox config + //Use default server for as Epicbox config // } // else { @@ -155,12 +156,12 @@ class EpiccashWallet extends Bip39Wallet { config["api_listen_port"] = port; config["api_listen_interface"] = nodeApiAddress.replaceFirst(uri.scheme, ""); - String stringConfig = jsonEncode(config); + final String stringConfig = jsonEncode(config); return stringConfig; } Future _currentWalletDirPath() async { - Directory appDir = await StackFileSystem.applicationRootDirectory(); + final Directory appDir = await StackFileSystem.applicationRootDirectory(); final path = "${appDir.path}/epiccash"; final String name = walletId.trim(); @@ -175,7 +176,7 @@ class EpiccashWallet extends Bip39Wallet { try { final available = info.cachedBalance.spendable.raw.toInt(); - var transactionFees = await epiccash.LibEpiccash.getTransactionFees( + final transactionFees = await epiccash.LibEpiccash.getTransactionFees( wallet: wallet!, amount: satoshiAmount, minimumConfirmations: cryptoCurrency.minConfirms, @@ -231,48 +232,33 @@ class EpiccashWallet extends Bip39Wallet { ); } - Future _testEpicboxServer(String host, int port) async { - // TODO use an EpicBoxServerModel as the only param - final websocketConnectionUri = 'wss://$host:$port'; - const connectionOptions = SocketConnectionOptions( - pingIntervalMs: 3000, - timeoutConnectionMs: 4000, + Future _testEpicboxServer(EpicBoxConfigModel epicboxConfig) async { + final host = epicboxConfig.host; + final port = epicboxConfig.port ?? 443; + WebSocketChannel? channel; + try { + final uri = Uri.parse('wss://$host:$port'); - /// see ping/pong messages in [logEventStream] stream - skipPingMessages: true, + channel = WebSocketChannel.connect( + uri, + ); - /// Set this attribute to `true` if do not need any ping/pong - /// messages and ping measurement. Default is `false` - pingRestrictionForce: true, - ); + await channel.ready; - final IMessageProcessor textSocketProcessor = - SocketSimpleTextProcessor(); - final textSocketHandler = IWebSocketHandler.createClient( - websocketConnectionUri, - textSocketProcessor, - connectionOptions: connectionOptions, - ); + final response = await channel.stream.first.timeout( + const Duration(seconds: 2), + ); - // Listening to server responses: - bool isConnected = true; - textSocketHandler.incomingMessagesStream.listen((inMsg) { + return response is String && response.contains("Challenge"); + } catch (_) { Logging.instance.log( - '> webSocket got text message from server: "$inMsg" ' - '[ping: ${textSocketHandler.pingDelayMs}]', - level: LogLevel.Info); - }); - - // Connecting to server: - final isTextSocketConnected = await textSocketHandler.connect(); - if (!isTextSocketConnected) { - // ignore: avoid_print - Logging.instance.log( - 'Connection to [$websocketConnectionUri] failed for some reason!', - level: LogLevel.Error); - isConnected = false; + "_testEpicBoxConnection failed on \"$host:$port\"", + level: LogLevel.Info, + ); + return false; + } finally { + await channel?.sink.close(); } - return isConnected; } Future _putSendToAddresses( @@ -318,27 +304,36 @@ class EpiccashWallet extends Bip39Wallet { int index, ) async { Address? address = await getCurrentReceivingAddress(); - EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); if (address != null) { final splitted = address.value.split('@'); //Check if the address is the same as the current epicbox domain //Since we're only using one epicbpox now this doesn't apply but will be // useful in the future + final epicboxConfig = await getEpicBoxConfig(); if (splitted[1] != epicboxConfig.host) { //Update the address address = await thisWalletAddress(index, epicboxConfig); } } else { + final epicboxConfig = await getEpicBoxConfig(); address = await thisWalletAddress(index, epicboxConfig); } + + if (info.cachedReceivingAddress != address.value) { + await info.updateReceivingAddress( + newAddress: address.value, + isar: mainDB.isar, + ); + } return address; } - Future

thisWalletAddress(int index, EpicBoxConfigModel epicboxConfig) async { - final wallet = - await secureStorageInterface.read(key: '${walletId}_wallet'); - // EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); + Future
thisWalletAddress( + int index, + EpicBoxConfigModel epicboxConfig, + ) async { + final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); final walletAddress = await epiccash.LibEpiccash.getAddressInfo( wallet: wallet!, @@ -360,7 +355,6 @@ class EpiccashWallet extends Bip39Wallet { subType: AddressSubType.receiving, publicKey: [], // ?? ); - await mainDB.updateOrPutAddresses([address]); return address; } @@ -390,7 +384,7 @@ class EpiccashWallet extends Bip39Wallet { level: LogLevel.Info, ); - int nextScannedBlock = await epiccash.LibEpiccash.scanOutputs( + final int nextScannedBlock = await epiccash.LibEpiccash.scanOutputs( wallet: wallet!, startHeight: lastScannedBlock, numberOfBlocks: scanChunkSize, @@ -430,7 +424,7 @@ class EpiccashWallet extends Bip39Wallet { Future _listenToEpicbox() async { Logging.instance.log("STARTING WALLET LISTENER ....", level: LogLevel.Info); final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); - EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); + final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); epiccash.LibEpiccash.startEpicboxListener( wallet: wallet!, epicboxConfig: epicboxConfig.toString(), @@ -444,7 +438,7 @@ class EpiccashWallet extends Bip39Wallet { ); if (Platform.isIOS) { final walletDir = await _currentWalletDirPath(); - var editConfig = jsonDecode(config as String); + final editConfig = jsonDecode(config as String); editConfig["wallet_dir"] = walletDir; config = jsonEncode(editConfig); @@ -454,14 +448,11 @@ class EpiccashWallet extends Bip39Wallet { // TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index int _calculateRestoreHeightFrom({required DateTime date}) { - int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; + final int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000; const int epicCashFirstBlock = 1565370278; const double overestimateSecondsPerBlock = 61; - int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; - int approximateHeight = chosenSeconds ~/ overestimateSecondsPerBlock; - //todo: check if print needed - // debugPrint( - // "approximate height: $approximateHeight chosen_seconds: $chosenSeconds"); + final int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock; + final int approximateHeight = chosenSeconds ~/ overestimateSecondsPerBlock; int height = approximateHeight; if (height < 0) { height = 0; @@ -509,7 +500,7 @@ class EpiccashWallet extends Bip39Wallet { await secureStorageInterface.write( key: '${walletId}_epicboxConfig', value: epicboxConfig.toString()); - String name = walletId; + final String name = walletId; await epiccash.LibEpiccash.initializeNewWallet( config: stringConfig, @@ -593,8 +584,9 @@ class EpiccashWallet extends Bip39Wallet { if (!receiverAddress.startsWith("http://") || !receiverAddress.startsWith("https://")) { - bool isEpicboxConnected = await _testEpicboxServer( - epicboxConfig.host, epicboxConfig.port ?? 443); + final bool isEpicboxConnected = await _testEpicboxServer( + epicboxConfig, + ); if (!isEpicboxConnected) { throw Exception("Failed to send TX : Unable to reach epicbox server"); } @@ -934,7 +926,6 @@ class EpiccashWallet extends Bip39Wallet { .findAll(); final myAddressesSet = myAddresses.toSet(); - final transactions = await epiccash.LibEpiccash.getTransactions( wallet: wallet!, refreshFromNode: refreshFromNode, @@ -975,7 +966,7 @@ class EpiccashWallet extends Bip39Wallet { ], walletOwns: true, ); - InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, @@ -1112,7 +1103,7 @@ class EpiccashWallet extends Bip39Wallet { @override Future estimateFeeFor(Amount amount, int feeRate) async { // setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function????? - int currentFee = await _nativeFee( + final int currentFee = await _nativeFee( amount.raw.toInt(), ifErrorEstimateFee: true, ); @@ -1157,13 +1148,13 @@ Future deleteEpicWallet({ final wallet = await secureStore.read(key: '${walletId}_wallet'); String? config = await secureStore.read(key: '${walletId}_config'); if (Platform.isIOS) { - Directory appDir = await StackFileSystem.applicationRootDirectory(); + final Directory appDir = await StackFileSystem.applicationRootDirectory(); final path = "${appDir.path}/epiccash"; final String name = walletId.trim(); final walletDir = '$path/$name'; - var editConfig = jsonDecode(config as String); + final editConfig = jsonDecode(config as String); editConfig["wallet_dir"] = walletDir; config = jsonEncode(editConfig); diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index ae02c5292..ea109fe61 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -62,14 +62,15 @@ class FiroWallet extends Bip39HDWallet @override Future updateTransactions() async { - List
allAddressesOld = await fetchAddressesForElectrumXScan(); + final List
allAddressesOld = + await fetchAddressesForElectrumXScan(); - Set receivingAddresses = allAddressesOld + final Set receivingAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.receiving) .map((e) => convertAddressString(e.value)) .toSet(); - Set changeAddresses = allAddressesOld + final Set changeAddresses = allAddressesOld .where((e) => e.subType == AddressSubType.change) .map((e) => convertAddressString(e.value)) .toSet(); @@ -98,7 +99,7 @@ class FiroWallet extends Bip39HDWallet } } - List> allTransactions = []; + final List> allTransactions = []; // some lelantus transactions aren't fetched via wallet addresses so they // will never show as confirmed in the gui. @@ -177,7 +178,7 @@ class FiroWallet extends Bip39HDWallet bool isMint = false; bool isJMint = false; bool isSparkMint = false; - bool isMasterNodePayment = false; + final bool isMasterNodePayment = false; final bool isSparkSpend = txData["type"] == 9 && txData["version"] == 3; final bool isMySpark = sparkTxids.contains(txData["txid"] as String); @@ -548,8 +549,12 @@ class FiroWallet extends Bip39HDWallet } @override - Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + Future< + ({ + String? blockedReason, + bool blocked, + String? utxoLabel, + })> checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map? jsonTX, @@ -557,30 +562,26 @@ class FiroWallet extends Bip39HDWallet ) async { bool blocked = false; String? blockedReason; - // - // if (jsonTX != null) { - // // check for bip47 notification - // final outputs = jsonTX["vout"] as List; - // for (final output in outputs) { - // List? scriptChunks = - // (output['scriptPubKey']?['asm'] as String?)?.split(" "); - // if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { - // final blindedPaymentCode = scriptChunks![1]; - // final bytes = blindedPaymentCode.toUint8ListFromHex; - // - // // https://en.bitcoin.it/wiki/BIP_0047#Sending - // if (bytes.length == 80 && bytes.first == 1) { - // blocked = true; - // blockedReason = "Paynym notification output. Incautious " - // "handling of outputs from notification transactions " - // "may cause unintended loss of privacy."; - // break; - // } - // } - // } - // } - // - return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); + String? label; + + if (jsonUTXO["value"] is int) { + // TODO: [prio=med] use special electrumx call to verify the 1000 Firo output is masternode + blocked = Amount.fromDecimal( + Decimal.fromInt( + 1000, // 1000 firo output is a possible master node + ), + fractionDigits: cryptoCurrency.fractionDigits, + ).raw == + BigInt.from(jsonUTXO["value"] as int); + + if (blocked) { + blockedReason = "Possible masternode output. " + "Unlock and spend at your own risk."; + label = "Possible masternode"; + } + } + + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: label); } @override @@ -620,6 +621,7 @@ class FiroWallet extends Bip39HDWallet final sparkAnonSetFuture = electrumXCachedClient.getSparkAnonymitySet( groupId: latestSparkCoinId.toString(), coin: info.coin, + useOnlyCacheIfNotEmpty: false, ); final sparkUsedCoinTagsFuture = electrumXCachedClient.getSparkUsedCoinsTags( @@ -632,9 +634,11 @@ class FiroWallet extends Bip39HDWallet level: LogLevel.Info, ); + final canBatch = await serverCanBatch; + for (final type in cryptoCurrency.supportedDerivationPathTypes) { receiveFutures.add( - serverCanBatch + canBatch ? checkGapsBatched( txCountBatchSize, root, @@ -656,7 +660,7 @@ class FiroWallet extends Bip39HDWallet ); for (final type in cryptoCurrency.supportedDerivationPathTypes) { changeFutures.add( - serverCanBatch + canBatch ? checkGapsBatched( txCountBatchSize, root, diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 89faa8950..8c4a12a28 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; @@ -7,6 +9,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/extensions/impl/uint8_list.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart'; @@ -193,8 +196,8 @@ class ParticlWallet extends Bip39HDWallet OutpointV2? outpoint; final coinbase = map["coinbase"] as String?; - - if (coinbase == null) { + final txType = map['type'] as String?; + if (coinbase == null && txType == null) { // Not a coinbase (ie a typical input). final txid = map["txid"] as String; final vout = map["vout"] as int; @@ -340,20 +343,92 @@ class ParticlWallet extends Bip39HDWallet Logging.instance.log("Starting Particl buildTransaction ----------", level: LogLevel.Info); - // TODO: use coinlib + // TODO: use coinlib (For this we need coinlib to support particl) + + final convertedNetwork = bitcoindart.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: bitcoindart.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); + + final List<({Uint8List? output, Uint8List? redeem})> extraData = []; + for (int i = 0; i < utxoSigningData.length; i++) { + final sd = utxoSigningData[i]; + + final pubKey = sd.keyPair!.publicKey.data; + final bitcoindart.PaymentData? data; + Uint8List? redeem, output; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = bitcoindart + .P2PKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip49: + final p2wpkh = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + redeem = p2wpkh.output; + data = bitcoindart + .P2SH( + data: bitcoindart.PaymentData(redeem: p2wpkh), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip84: + // input = coinlib.P2WPKHInput( + // prevOut: coinlib.OutPoint.fromHex(sd.utxo.txid, sd.utxo.vout), + // publicKey: keys.publicKey, + // ); + data = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip86: + data = null; + break; + + default: + throw Exception("DerivePathType unsupported"); + } + + // sd.output = input.script!.compiled; + + if (sd.derivePathType != DerivePathType.bip86) { + output = data!.output!; + } + + extraData.add((output: output, redeem: redeem)); + } final txb = bitcoindart.TransactionBuilder( - network: bitcoindart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ), + network: convertedNetwork, ); const version = 160; // buildTransaction overridden for Particl to set this. // TODO: [prio=low] refactor overridden buildTransaction to use eg. cryptocurrency.networkParams.txVersion. @@ -370,7 +445,7 @@ class ParticlWallet extends Bip39HDWallet txid, utxoSigningData[i].utxo.vout, null, - utxoSigningData[i].output!, + extraData[i].output!, cryptoCurrency.networkParams.bech32Hrp, ); @@ -427,9 +502,13 @@ class ParticlWallet extends Bip39HDWallet for (var i = 0; i < utxoSigningData.length; i++) { txb.sign( vin: i, - keyPair: utxoSigningData[i].keyPair!, + keyPair: bitcoindart.ECPair.fromPrivateKey( + utxoSigningData[i].keyPair!.privateKey.data, + network: convertedNetwork, + compressed: utxoSigningData[i].keyPair!.privateKey.compressed, + ), witnessValue: utxoSigningData[i].utxo.value, - redeemScript: utxoSigningData[i].redeemScript, + redeemScript: extraData[i].redeem, overridePrefix: cryptoCurrency.networkParams.bech32Hrp, ); } diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart new file mode 100644 index 000000000..e08566f45 --- /dev/null +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -0,0 +1,297 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/peercoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; + +class PeercoinWallet extends Bip39HDWallet + with ElectrumXInterface, CoinControlInterface { + @override + int get isarTransactionVersion => 2; + + PeercoinWallet(CryptoCurrencyNetwork network) : super(Peercoin(network)); + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + // =========================================================================== + + @override + Future> fetchAddressesForElectrumXScan() async { + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); + return allAddresses; + } + + // =========================================================================== + + @override + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + // TODO: actually do this properly for peercoin + // this is probably wrong for peercoin + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil(), + ), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + /// we can just pretend vSize is size for peercoin + @override + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } + + // =========================================================================== + + @override + Future< + ({ + bool blocked, + String? blockedReason, + String? utxoLabel, + })> checkBlockUTXO( + Map jsonUTXO, + String? scriptPubKeyHex, + Map jsonTX, + String? utxoOwnerAddress, + ) async { + // TODO [prio=high]: Check if Peercoin has outputs (eg stakes etc) to block. + return (blocked: false, blockedReason: null, utxoLabel: null); + } + + @override + Future updateTransactions() async { + // Get all addresses. + final List
allAddressesOld = + await fetchAddressesForElectrumXScan(); + + // Separate receiving and change addresses. + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); + + // Remove duplicates. + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + + // Fetch history from ElectrumX. + final List> allTxHashes = + await fetchHistory(allAddressesSet); + + // Only parse new txs (not in db yet). + final List> allTransactions = []; + for (final txHash in allTxHashes) { + // Check for duplicates by searching for tx by tx_hash in db. + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); + + if (storedTx == null || + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0)) { + // Tx not in db yet. + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: cryptoCurrency.coin, + ); + + // Only tx to list once. + if (allTransactions + .indexWhere((e) => e["txid"] == tx["txid"] as String) == + -1) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + } + + // Parse all new txs. + final List txns = []; + for (final txData in allTransactions) { + bool wasSentFromThisWallet = false; + // Set to true if any inputs were detected as owned by this wallet. + + bool wasReceivedInThisWallet = false; + // Set to true if any outputs were detected as owned by this wallet. + + // Parse inputs. + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + if (coinbase == null) { + // Not a coinbase (ie a typical input). + final txid = map["txid"] as String; + final vout = map["vout"] as int; + + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + coin: cryptoCurrency.coin, + ); + + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map, + ); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + walletOwns: false, // Doesn't matter here as this is not saved. + ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } + + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + scriptSigAsm: map["scriptSig"]?["asm"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // Check if input was from this wallet. + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + // Parse outputs. + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // If output was to my wallet, add value to amount received. + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + + outputs.add(output); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + const TransactionSubType subType = TransactionSubType.none; + + // At least one input was owned by this wallet. + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { + // Definitely sent all to self. + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // Most likely just a typical send, do nothing here yet. + } + + // Namecoin doesn't have special outputs like tokens, ordinals, etc. + // But this is where you'd check for special outputs. + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.log( + "Unexpected tx found (ignoring it): $txData", + level: LogLevel.Error, + ); + continue; + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: null, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } +} diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart new file mode 100644 index 000000000..d2f3582c9 --- /dev/null +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -0,0 +1,478 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:decimal/decimal.dart'; +import 'package:isar/isar.dart'; +import 'package:socks5_proxy/socks_client.dart'; +import 'package:solana/dto.dart'; +import 'package:solana/solana.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' + as isar; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; +import 'package:tuple/tuple.dart'; + +class SolanaWallet extends Bip39Wallet { + SolanaWallet(CryptoCurrencyNetwork network) : super(Solana(network)); + + static const String _addressDerivationPath = "m/44'/501'/0'/0'"; + + NodeModel? _solNode; + + RpcClient? _rpcClient; // The Solana RpcClient. + + Future _getKeyPair() async { + return Ed25519HDKeyPair.fromMnemonic( + await getMnemonic(), + account: 0, + change: 0, + ); + } + + Future
_generateAddress() async { + final addressStruct = Address( + walletId: walletId, + value: (await _getKeyPair()).address, + publicKey: List.empty(), + derivationIndex: 0, + derivationPath: DerivationPath()..value = _addressDerivationPath, + type: cryptoCurrency.coin.primaryAddressType, + subType: AddressSubType.receiving, + ); + return addressStruct; + } + + Future _getCurrentBalanceInLamports() async { + _checkClient(); + final balance = await _rpcClient?.getBalance((await _getKeyPair()).address); + return balance!.value; + } + + Future _getEstimatedNetworkFee(Amount transferAmount) async { + final latestBlockhash = await _rpcClient?.getLatestBlockhash(); + final pubKey = (await _getKeyPair()).publicKey; + + final compiledMessage = Message(instructions: [ + SystemInstruction.transfer( + fundingAccount: pubKey, + recipientAccount: pubKey, + lamports: transferAmount.raw.toInt(), + ) + ]).compile( + recentBlockhash: latestBlockhash!.value.blockhash, + feePayer: pubKey, + ); + + return await _rpcClient?.getFeeForMessage( + base64Encode(compiledMessage.toByteArray().toList()), + ); + } + + @override + FilterOperation? get changeAddressFilterOperation => + throw UnimplementedError(); + + @override + Future checkSaveInitialReceivingAddress() async { + try { + Address? address = await getCurrentReceivingAddress(); + + if (address == null) { + address = await _generateAddress(); + + await mainDB.updateOrPutAddresses([address]); + } + } catch (e, s) { + Logging.instance.log( + "$runtimeType checkSaveInitialReceivingAddress() failed: $e\n$s", + level: LogLevel.Error, + ); + } + } + + @override + Future prepareSend({required TxData txData}) async { + try { + _checkClient(); + + if (txData.recipients == null || txData.recipients!.length != 1) { + throw Exception("$runtimeType prepareSend requires 1 recipient"); + } + + final Amount sendAmount = txData.amount!; + + if (sendAmount > info.cachedBalance.spendable) { + throw Exception("Insufficient available balance"); + } + + final feeAmount = await _getEstimatedNetworkFee(sendAmount); + if (feeAmount == null) { + throw Exception( + "Failed to get fees, please check your node connection."); + } + + final address = await getCurrentReceivingAddress(); + + // Rent exemption of Solana + final accInfo = await _rpcClient?.getAccountInfo(address!.value); + final int minimumRent = + await _rpcClient?.getMinimumBalanceForRentExemption( + accInfo!.value!.data.toString().length) ?? + 0; // TODO revisit null condition. + if (minimumRent > + ((await _getCurrentBalanceInLamports()) - + txData.amount!.raw.toInt() - + feeAmount)) { + throw Exception( + "Insufficient remaining balance for rent exemption, minimum rent: " + "${minimumRent / pow(10, cryptoCurrency.fractionDigits)}", + ); + } + + return txData.copyWith( + fee: Amount( + rawValue: BigInt.from(feeAmount), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + } catch (e, s) { + Logging.instance.log( + "$runtimeType Solana prepareSend failed: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + @override + Future confirmSend({required TxData txData}) async { + try { + _checkClient(); + + final keyPair = await _getKeyPair(); + final recipientAccount = txData.recipients!.first; + final recipientPubKey = + Ed25519HDPublicKey.fromBase58(recipientAccount.address); + final message = Message( + instructions: [ + SystemInstruction.transfer( + fundingAccount: keyPair.publicKey, + recipientAccount: recipientPubKey, + lamports: txData.amount!.raw.toInt()), + ComputeBudgetInstruction.setComputeUnitPrice( + microLamports: txData.fee!.raw.toInt() - 5000), + // 5000 lamports is the base fee for a transaction. This instruction adds the necessary fee on top of base fee if it is needed. + ComputeBudgetInstruction.setComputeUnitLimit(units: 1000000), + // 1000000 is the multiplication number to turn the compute unit price of microLamports to lamports. + // These instructions also help the user to not pay more than the shown fee. + // See: https://solanacookbook.com/references/basic-transactions.html#how-to-change-compute-budget-fee-priority-for-a-transaction + ], + ); + + final txid = await _rpcClient?.signAndSendTransaction(message, [keyPair]); + return txData.copyWith( + txid: txid, + ); + } catch (e, s) { + Logging.instance.log( + "$runtimeType Solana confirmSend failed: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + @override + Future estimateFeeFor(Amount amount, int feeRate) async { + _checkClient(); + + if (info.cachedBalance.spendable.raw == BigInt.zero) { + return Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + final fee = await _getEstimatedNetworkFee(amount); + if (fee == null) { + throw Exception("Failed to get fees, please check your node connection."); + } + + return Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + Future get fees async { + _checkClient(); + + final fee = await _getEstimatedNetworkFee( + Amount.fromDecimal( + Decimal.one, // 1 SOL + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + if (fee == null) { + throw Exception("Failed to get fees, please check your node connection."); + } + + return FeeObject( + numberOfBlocksFast: 1, + numberOfBlocksAverage: 1, + numberOfBlocksSlow: 1, + fast: fee, + medium: fee, + slow: fee); + } + + @override + Future pingCheck() { + try { + _checkClient(); + _rpcClient?.getHealth(); + return Future.value(true); + } catch (e, s) { + Logging.instance.log( + "$runtimeType Solana pingCheck failed: $e\n$s", + level: LogLevel.Error, + ); + return Future.value(false); + } + } + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + @override + Future recover({required bool isRescan}) async { + await refreshMutex.protect(() async { + final addressStruct = await _generateAddress(); + + await mainDB.updateOrPutAddresses([addressStruct]); + + if (info.cachedReceivingAddress != addressStruct.value) { + await info.updateReceivingAddress( + newAddress: addressStruct.value, + isar: mainDB.isar, + ); + } + + await Future.wait([ + updateBalance(), + updateChainHeight(), + updateTransactions(), + ]); + }); + } + + @override + Future updateBalance() async { + try { + final address = await getCurrentReceivingAddress(); + _checkClient(); + + final balance = await _rpcClient?.getBalance(address!.value); + + // Rent exemption of Solana + final accInfo = await _rpcClient?.getAccountInfo(address!.value); + // TODO [prio=low]: handle null account info. + final int minimumRent = + await _rpcClient?.getMinimumBalanceForRentExemption( + accInfo!.value!.data.toString().length) ?? + 0; + // TODO [prio=low]: revisit null condition. + final spendableBalance = balance!.value - minimumRent; + + final newBalance = Balance( + total: Amount( + rawValue: BigInt.from(balance.value), + fractionDigits: Coin.solana.decimals, + ), + spendable: Amount( + rawValue: BigInt.from(spendableBalance), + fractionDigits: Coin.solana.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.from(minimumRent), + fractionDigits: Coin.solana.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: Coin.solana.decimals, + ), + ); + + await info.updateBalance(newBalance: newBalance, isar: mainDB.isar); + } catch (e, s) { + Logging.instance.log( + "Error getting balance in solana_wallet.dart: $e\n$s", + level: LogLevel.Error, + ); + } + } + + @override + Future updateChainHeight() async { + try { + _checkClient(); + + final int blockHeight = await _rpcClient?.getSlot() ?? 0; + // TODO [prio=low]: Revisit null condition. + + await info.updateCachedChainHeight( + newHeight: blockHeight, + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.log( + "Error occurred in solana_wallet.dart while getting" + " chain height for solana: $e\n$s", + level: LogLevel.Error, + ); + } + } + + @override + Future updateNode() async { + _solNode = getCurrentNode(); + await refresh(); + } + + @override + NodeModel getCurrentNode() { + return _solNode ?? + NodeService(secureStorageInterface: secureStorageInterface) + .getPrimaryNodeFor(coin: info.coin) ?? + DefaultNodes.getNodeFor(info.coin); + } + + @override + Future updateTransactions() async { + try { + _checkClient(); + + final transactionsList = await _rpcClient?.getTransactionsList( + (await _getKeyPair()).publicKey, + encoding: Encoding.jsonParsed); + final txsList = + List>.empty(growable: true); + + final myAddress = (await getCurrentReceivingAddress())!; + + // TODO [prio=low]: Revisit null assertion below. + + for (final tx in transactionsList!) { + final senderAddress = + (tx.transaction as ParsedTransaction).message.accountKeys[0].pubkey; + var receiverAddress = + (tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey; + var txType = isar.TransactionType.unknown; + final txAmount = Amount( + rawValue: + BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + if ((senderAddress == myAddress.value) && + (receiverAddress == "11111111111111111111111111111111")) { + // The account that is only 1's are System Program accounts which + // means there is no receiver except the sender, + // see: https://explorer.solana.com/address/11111111111111111111111111111111 + txType = isar.TransactionType.sentToSelf; + receiverAddress = senderAddress; + } else if (senderAddress == myAddress.value) { + txType = isar.TransactionType.outgoing; + } else if (receiverAddress == myAddress.value) { + txType = isar.TransactionType.incoming; + } + + final transaction = isar.Transaction( + walletId: walletId, + txid: (tx.transaction as ParsedTransaction).signatures[0], + timestamp: tx.blockTime!, + type: txType, + subType: isar.TransactionSubType.none, + amount: tx.meta!.postBalances[1] - tx.meta!.preBalances[1], + amountString: txAmount.toJsonString(), + fee: tx.meta!.fee, + height: tx.slot, + isCancelled: false, + isLelantus: false, + slateId: null, + otherData: null, + inputs: [], + outputs: [], + nonce: null, + numberOfMessages: 0, + ); + + final txAddress = Address( + walletId: walletId, + value: receiverAddress, + publicKey: List.empty(), + derivationIndex: 0, + derivationPath: DerivationPath()..value = _addressDerivationPath, + type: AddressType.solana, + subType: txType == isar.TransactionType.outgoing + ? AddressSubType.unknown + : AddressSubType.receiving, + ); + + txsList.add(Tuple2(transaction, txAddress)); + } + await mainDB.addNewTransactionData(txsList, walletId); + } catch (e, s) { + Logging.instance.log( + "Error occurred in solana_wallet.dart while getting" + " transactions for solana: $e\n$s", + level: LogLevel.Error, + ); + } + } + + @override + Future updateUTXOs() { + // No UTXOs in Solana + return Future.value(false); + } + + /// Make sure the Solana RpcClient uses Tor if it's enabled. + /// + void _checkClient() async { + HttpClient? httpClient; + + if (prefs.useTor) { + // Make proxied HttpClient. + final ({InternetAddress host, int port}) proxyInfo = + TorService.sharedInstance.getProxyInfo(); + + final proxySettings = ProxySettings(proxyInfo.host, proxyInfo.port); + httpClient = HttpClient(); + SocksTCPClient.assignToHttpClient(httpClient, [proxySettings]); + } + + _rpcClient = RpcClient( + "${getCurrentNode().host}:${getCurrentNode().port}", + timeout: const Duration(seconds: 30), + customHeaders: {}, + httpClient: httpClient, + ); + return; + } +} diff --git a/lib/wallets/wallet/impl/stellar_wallet.dart b/lib/wallets/wallet/impl/stellar_wallet.dart index d078dd98e..657fd7676 100644 --- a/lib/wallets/wallet/impl/stellar_wallet.dart +++ b/lib/wallets/wallet/impl/stellar_wallet.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:isar/isar.dart'; +import 'package:mutex/mutex.dart'; +import 'package:socks5_proxy/socks.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; @@ -9,6 +12,10 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart' import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -20,11 +27,47 @@ import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart' as stellar; class StellarWallet extends Bip39Wallet { - StellarWallet(CryptoCurrencyNetwork network) : super(Stellar(network)); + StellarWallet(CryptoCurrencyNetwork network) : super(Stellar(network)) { + final bus = GlobalEventBus.instance; - stellar.StellarSDK get stellarSdk { - if (_stellarSdk == null) { - _updateSdk(); + // Listen for tor status changes. + _torStatusListener = bus.on().listen( + (event) async { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + if (!_torConnectingLock.isLocked) { + await _torConnectingLock.acquire(); + } + _requireMutex = true; + break; + + case TorConnectionStatus.connected: + case TorConnectionStatus.disconnected: + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + _requireMutex = false; + break; + } + }, + ); + + // Listen for tor preference changes. + _torPreferenceListener = bus.on().listen( + (event) async { + _stellarSdk?.httpClient.close(); + _stellarSdk = null; + }, + ); + } + + Future get stellarSdk async { + if (_requireMutex) { + await _torConnectingLock.protect(() async { + _stellarSdk ??= _getFreshSdk(); + }); + } else { + _stellarSdk ??= _getFreshSdk(); } return _stellarSdk!; } @@ -41,24 +84,60 @@ class StellarWallet extends Bip39Wallet { } // ============== Private ==================================================== + // add finalizer to cancel stream subscription when all references to an + // instance of this becomes inaccessible + final _ = Finalizer( + (p0) { + p0._torPreferenceListener?.cancel(); + p0._torStatusListener?.cancel(); + }, + ); + + StreamSubscription? _torStatusListener; + StreamSubscription? _torPreferenceListener; + + final Mutex _torConnectingLock = Mutex(); + bool _requireMutex = false; stellar.StellarSDK? _stellarSdk; Future _getBaseFee() async { - final fees = await stellarSdk.feeStats.execute(); + final fees = await (await stellarSdk).feeStats.execute(); return int.parse(fees.lastLedgerBaseFee); } - void _updateSdk() { + stellar.StellarSDK _getFreshSdk() { final currentNode = getCurrentNode(); - _stellarSdk = stellar.StellarSDK("${currentNode.host}:${currentNode.port}"); + HttpClient? _httpClient; + + if (prefs.useTor) { + final ({InternetAddress host, int port}) proxyInfo = + TorService.sharedInstance.getProxyInfo(); + + _httpClient = HttpClient(); + SocksTCPClient.assignToHttpClient( + _httpClient, + [ + ProxySettings( + proxyInfo.host, + proxyInfo.port, + ), + ], + ); + } + + return stellar.StellarSDK( + "${currentNode.host}:${currentNode.port}", + httpClient: _httpClient, + ); } Future _accountExists(String accountId) async { bool exists = false; try { - final receiverAccount = await stellarSdk.accounts.account(accountId); + final receiverAccount = + await (await stellarSdk).accounts.account(accountId); if (receiverAccount.accountId != "") { exists = true; } @@ -165,7 +244,8 @@ class StellarWallet extends Bip39Wallet { @override Future confirmSend({required TxData txData}) async { final senderKeyPair = await _getSenderKeyPair(index: 0); - final sender = await stellarSdk.accounts.account(senderKeyPair.accountId); + final sender = + await (await stellarSdk).accounts.account(senderKeyPair.accountId); final address = txData.recipients!.first.address; final amountToSend = txData.recipients!.first.amount; @@ -203,7 +283,7 @@ class StellarWallet extends Bip39Wallet { transaction.sign(senderKeyPair, stellarNetwork); try { - final response = await stellarSdk.submitTransaction(transaction); + final response = await (await stellarSdk).submitTransaction(transaction); if (!response.success) { throw Exception("${response.extras?.resultCodes?.transactionResultCode}" " ::: ${response.extras?.resultCodes?.operationsResultCodes}"); @@ -230,7 +310,7 @@ class StellarWallet extends Bip39Wallet { @override Future get fees async { - int fee = await _getBaseFee(); + final int fee = await _getBaseFee(); return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 1, @@ -268,7 +348,8 @@ class StellarWallet extends Bip39Wallet { stellar.AccountResponse accountResponse; try { - accountResponse = await stellarSdk.accounts + accountResponse = await (await stellarSdk) + .accounts .account((await getCurrentReceivingAddress())!.value) .onError((error, stackTrace) => throw error!); } catch (e) { @@ -289,7 +370,7 @@ class StellarWallet extends Bip39Wallet { } } - for (stellar.Balance balance in accountResponse.balances) { + for (final stellar.Balance balance in accountResponse.balances) { switch (balance.assetType) { case stellar.Asset.TYPE_NATIVE: final swBalance = Balance( @@ -326,7 +407,8 @@ class StellarWallet extends Bip39Wallet { @override Future updateChainHeight() async { try { - final height = await stellarSdk.ledgers + final height = await (await stellarSdk) + .ledgers .order(stellar.RequestBuilderOrder.DESC) .limit(1) .execute() @@ -344,7 +426,8 @@ class StellarWallet extends Bip39Wallet { @override Future updateNode() async { - _updateSdk(); + _stellarSdk?.httpClient.close(); + _stellarSdk = _getFreshSdk(); } @override @@ -352,10 +435,11 @@ class StellarWallet extends Bip39Wallet { try { final myAddress = (await getCurrentReceivingAddress())!; - List transactionList = []; + final List transactionList = []; stellar.Page payments; try { - payments = await stellarSdk.payments + payments = await (await stellarSdk) + .payments .forAccount(myAddress.value) .order(stellar.RequestBuilderOrder.DESC) .execute(); @@ -375,7 +459,7 @@ class StellarWallet extends Bip39Wallet { rethrow; } } - for (stellar.OperationResponse response in payments.records!) { + for (final stellar.OperationResponse response in payments.records!) { // PaymentOperationResponse por; if (response is stellar.PaymentOperationResponse) { final por = response; @@ -405,7 +489,8 @@ class StellarWallet extends Bip39Wallet { final List outputs = []; final List inputs = []; - OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + final OutputV2 output = + OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), addresses: [ @@ -413,7 +498,7 @@ class StellarWallet extends Bip39Wallet { ], walletOwns: addressTo == myAddress.value, ); - InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, @@ -433,8 +518,9 @@ class StellarWallet extends Bip39Wallet { int height = 0; //Query the transaction linked to the payment, // por.transaction returns a null sometimes - stellar.TransactionResponse tx = - await stellarSdk.transactions.transaction(por.transactionHash!); + final stellar.TransactionResponse tx = await (await stellarSdk) + .transactions + .transaction(por.transactionHash!); if (tx.hash.isNotEmpty) { fee = tx.feeCharged!; @@ -485,7 +571,8 @@ class StellarWallet extends Bip39Wallet { final List outputs = []; final List inputs = []; - OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + final OutputV2 output = + OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), addresses: [ @@ -494,7 +581,7 @@ class StellarWallet extends Bip39Wallet { ], walletOwns: caor.sourceAccount! == myAddress.value, ); - InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, sequence: null, @@ -515,8 +602,9 @@ class StellarWallet extends Bip39Wallet { int fee = 0; int height = 0; - final tx = - await stellarSdk.transactions.transaction(caor.transactionHash!); + final tx = await (await stellarSdk) + .transactions + .transaction(caor.transactionHash!); if (tx.hash.isNotEmpty) { fee = tx.feeCharged!; height = tx.ledger; diff --git a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart index 8244c5fd9..d5c4bc31d 100644 --- a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart +++ b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart @@ -92,16 +92,8 @@ class EthTokenWallet extends Wallet { ); } - // String? mnemonicString = await ethWallet.getMnemonic(); - // - // //Get private key for given mnemonic - // String privateKey = getPrivateKey( - // mnemonicString, - // (await ethWallet.getMnemonicPassphrase()), - // ); - // _credentials = web3dart.EthPrivateKey.fromHex(privateKey); - try { + // try parse abi and extract transfer function _deployedContract = web3dart.DeployedContract( ContractAbiExtensions.fromJsonList( jsonList: tokenContract.abi!, @@ -109,90 +101,51 @@ class EthTokenWallet extends Wallet { ), contractAddress, ); - } catch (_) { - rethrow; - } - - try { _sendFunction = _deployedContract.function('transfer'); } catch (_) { - //==================================================================== - // final list = List>.from( - // jsonDecode(tokenContract.abi!) as List); - // final functionNames = list.map((e) => e["name"] as String); - // - // if (!functionNames.contains("balanceOf")) { - // list.add( - // { - // "encoding": "0x70a08231", - // "inputs": [ - // {"name": "account", "type": "address"} - // ], - // "name": "balanceOf", - // "outputs": [ - // {"name": "val_0", "type": "uint256"} - // ], - // "signature": "balanceOf(address)", - // "type": "function" - // }, - // ); - // } - // - // if (!functionNames.contains("transfer")) { - // list.add( - // { - // "encoding": "0xa9059cbb", - // "inputs": [ - // {"name": "dst", "type": "address"}, - // {"name": "rawAmount", "type": "uint256"} - // ], - // "name": "transfer", - // "outputs": [ - // {"name": "val_0", "type": "bool"} - // ], - // "signature": "transfer(address,uint256)", - // "type": "function" - // }, - // ); - // } - //-------------------------------------------------------------------- - //==================================================================== - - // function not found so likely a proxy so we need to fetch the impl - //==================================================================== - // final updatedToken = tokenContract.copyWith(abi: jsonEncode(list)); - // // Store updated contract - // final id = await MainDB.instance.putEthContract(updatedToken); - // _tokenContract = updatedToken..id = id; - //-------------------------------------------------------------------- - final contractAddressResponse = - await EthereumAPI.getProxyTokenImplementationAddress( - contractAddress.hex); - - if (contractAddressResponse.value != null) { - _tokenContract = await _updateTokenABI( - forContract: tokenContract, - usingContractAddress: contractAddressResponse.value!, - ); - } else { - throw contractAddressResponse.exception!; - } - //==================================================================== - } - - try { - _deployedContract = web3dart.DeployedContract( - ContractAbiExtensions.fromJsonList( - jsonList: tokenContract.abi!, - name: tokenContract.name, - ), - contractAddress, + // some failure so first try to make sure we have the latest abi + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddress.hex, ); - } catch (_) { - rethrow; - } - _sendFunction = _deployedContract.function('transfer'); + try { + // try again to parse abi and extract transfer function + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + _sendFunction = _deployedContract.function('transfer'); + } catch (_) { + // if it fails again we check if there is a proxy token impl and + // then try one last time to update and parse the abi + final contractAddressResponse = + await EthereumAPI.getProxyTokenImplementationAddress( + contractAddress.hex); + + if (contractAddressResponse.value != null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddressResponse.value!, + ); + } else { + throw contractAddressResponse.exception!; + } + + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + + _sendFunction = _deployedContract.function('transfer'); + } + } } catch (e, s) { Logging.instance.log( "$runtimeType wallet failed init(): $e\n$s", diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index d1df08502..bdf3374a8 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:isar/isar.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; @@ -5,6 +7,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -105,9 +108,12 @@ class TezosWallet extends Bip39Wallet { // print("COUNTER: $counter"); // print("customFee: $customFee"); // } - final tezartClient = tezart.TezartClient( - server, - ); + ({InternetAddress host, int port})? proxyInfo = + prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null; + final tezartClient = tezart.TezartClient(server, + proxy: proxyInfo != null + ? "socks5://${proxyInfo.host}:${proxyInfo.port};" + : null); final opList = await tezartClient.transferOperation( source: sourceKeyStore, diff --git a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart index 8d352811b..ffb927713 100644 --- a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart +++ b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart @@ -11,7 +11,13 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address abstract class Bip39HDWallet extends Bip39Wallet with MultiAddressInterface { - Bip39HDWallet(T cryptoCurrency) : super(cryptoCurrency); + Bip39HDWallet(super.cryptoCurrency); + + Set get supportedAddressTypes => + cryptoCurrency.supportedDerivationPathTypes + .where((e) => e != DerivePathType.bip49) + .map((e) => e.getAddressType()) + .toSet(); Future getRootHDNode() async { final seed = bip39.mnemonicToSeed( @@ -21,6 +27,33 @@ abstract class Bip39HDWallet extends Bip39Wallet return coinlib.HDPrivateKey.fromSeed(seed); } + Future
generateNextReceivingAddress({ + required DerivePathType derivePathType, + }) async { + if (!cryptoCurrency.supportedDerivationPathTypes.contains(derivePathType)) { + throw Exception( + "Unsupported DerivePathType passed to generateNextReceivingAddress().", + ); + } + + final current = await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(derivePathType.getAddressType()) + .sortByDerivationIndexDesc() + .findFirst(); + final index = current == null ? 0 : current.derivationIndex + 1; + const chain = 0; // receiving address + final address = await _generateAddress( + chain: chain, + index: index, + derivePathType: derivePathType, + ); + + return address; + } + /// Generates a receiving address. If none /// are in the current wallet db it will generate at index 0, otherwise the /// highest index found in the current wallet db. diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 2f1691b0a..549bfefb2 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/isar/models/wallet_info.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/dogecoin_wallet.dart'; @@ -38,6 +39,8 @@ import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/namecoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/particl_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/peercoin_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/solana_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/stellar_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/tezos_wallet.dart'; @@ -55,6 +58,9 @@ abstract class Wallet { // default to Transaction class. For TransactionV2 set to 2 int get isarTransactionVersion => 1; + // whether the wallet currently supports multiple recipients per tx + bool get supportsMultiRecipient => false; + Wallet(this.cryptoCurrency); //============================================================================ @@ -289,7 +295,7 @@ abstract class Wallet { wallet.prefs = prefs; wallet.nodeService = nodeService; - if (wallet is ElectrumXInterface) { + if (wallet is ElectrumXInterface || wallet is BitcoinFrostWallet) { // initialize electrumx instance await wallet.updateNode(); } @@ -312,6 +318,11 @@ abstract class Wallet { case Coin.bitcoinTestNet: return BitcoinWallet(CryptoCurrencyNetwork.test); + case Coin.bitcoinFrost: + return BitcoinFrostWallet(CryptoCurrencyNetwork.main); + case Coin.bitcoinFrostTestNet: + return BitcoinFrostWallet(CryptoCurrencyNetwork.test); + case Coin.bitcoincash: return BitcoincashWallet(CryptoCurrencyNetwork.main); case Coin.bitcoincashTestnet: @@ -353,6 +364,14 @@ abstract class Wallet { case Coin.particl: return ParticlWallet(CryptoCurrencyNetwork.main); + case Coin.peercoin: + return PeercoinWallet(CryptoCurrencyNetwork.main); + case Coin.peercoinTestNet: + return PeercoinWallet(CryptoCurrencyNetwork.test); + + case Coin.solana: + return SolanaWallet(CryptoCurrencyNetwork.main); + case Coin.stellar: return StellarWallet(CryptoCurrencyNetwork.main); case Coin.stellarTestnet: @@ -384,10 +403,10 @@ abstract class Wallet { } void _periodicPingCheck() async { - bool hasNetwork = await pingCheck(); + final bool hasNetwork = await pingCheck(); if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork + final NodeConnectionStatus status = hasNetwork ? NodeConnectionStatus.connected : NodeConnectionStatus.disconnected; GlobalEventBus.instance.fire( @@ -624,7 +643,7 @@ abstract class Wallet { // Close the subscription if this wallet is not in the list to be synced. if (!prefs.walletIdsSyncOnStartup.contains(walletId)) { // Check if there's another wallet of this coin on the sync list. - List walletIds = []; + final List walletIds = []; for (final id in prefs.walletIdsSyncOnStartup) { final wallet = mainDB.isar.walletInfo .where() diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart index e9a3ffab3..c94fd15f9 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart @@ -1,4 +1,5 @@ import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:bitbox/src/utils/network.dart' as bitbox_utils; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; @@ -89,8 +90,17 @@ mixin BCashInterface on Bip39HDWallet, ElectrumXInterface { try { // Sign the transaction accordingly for (int i = 0; i < utxoSigningData.length; i++) { - final bitboxEC = bitbox.ECPair.fromWIF( - utxoSigningData[i].keyPair!.toWIF(), + final bitboxEC = bitbox.ECPair.fromPrivateKey( + utxoSigningData[i].keyPair!.privateKey.data, + network: bitbox_utils.Network( + cryptoCurrency.networkParams.privHDPrefix, + cryptoCurrency.networkParams.pubHDPrefix, + cryptoCurrency.network == CryptoCurrencyNetwork.test, + cryptoCurrency.networkParams.p2pkhPrefix, + cryptoCurrency.networkParams.wifPrefix, + cryptoCurrency.networkParams.p2pkhPrefix, + ), + compressed: utxoSigningData[i].keyPair!.privateKey.compressed, ); builder.sign( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index a7bb2a1af..026e9f5fc 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1,14 +1,11 @@ import 'dart:async'; import 'dart:math'; +import 'dart:typed_data'; -import 'package:bip47/src/util.dart'; -import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; -import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter; -import 'package:electrum_adapter/electrum_adapter.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.dart'; +import 'package:stackwallet/electrumx_rpc/client_manager.dart'; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; @@ -16,39 +13,43 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2 import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/signing_data.dart'; -import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/paynym_is_api.dart'; -import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/peercoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; -import 'package:stream_channel/stream_channel.dart'; mixin ElectrumXInterface on Bip39HDWallet { late ElectrumXClient electrumXClient; - late StreamChannel electrumAdapterChannel; - late ElectrumClient electrumAdapterClient; late CachedElectrumXClient electrumXCachedClient; - // late SubscribableElectrumXClient subscribableElectrumXClient; - late ChainHeightServiceManager chainHeightServiceManager; int? get maximumFeerate => null; static const _kServerBatchCutoffVersion = [1, 6]; List? _serverVersion; - bool get serverCanBatch { + Future get serverCanBatch async { // Firo server added batching without incrementing version number... if (cryptoCurrency is Firo) { return true; } + + try { + _serverVersion ??= _parseServerVersion((await electrumXClient + .getServerFeatures() + .timeout(const Duration(seconds: 2)))["server_version"] as String); + } catch (_) { + // ignore failure as it doesn't matter + } + if (_serverVersion != null && _serverVersion!.length > 2) { if (_serverVersion![0] > _kServerBatchCutoffVersion[0]) { return true; @@ -202,8 +203,8 @@ mixin ElectrumXInterface on Bip39HDWallet { .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray - List recipientsArray = [recipientAddress]; - List recipientsAmtArray = [satoshiAmountToSend]; + final List recipientsArray = [recipientAddress]; + final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); @@ -246,6 +247,13 @@ mixin ElectrumXInterface on Bip39HDWallet { } final int amount = satoshiAmountToSend - feeForOneOutput; + + if (amount < 0) { + throw Exception( + "Estimated fee ($feeForOneOutput sats) is greater than balance!", + ); + } + final data = await buildTransaction( txData: txData.copyWith( recipients: await _helperRecipientsConvert( @@ -327,7 +335,7 @@ mixin ElectrumXInterface on Bip39HDWallet { feeForOneOutput + cryptoCurrency.dustLimit.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. - int changeOutputSize = + final int changeOutputSize = satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > cryptoCurrency.dustLimit satoshis, we perform the mechanics required to properly generate and use a new @@ -372,7 +380,7 @@ mixin ElectrumXInterface on Bip39HDWallet { // make sure minimum fee is accurate if that is being used if (txn.vSize! - feeBeingPaid == 1) { - int changeOutputSize = + final int changeOutputSize = satoshisBeingUsed - satoshiAmountToSend - txn.vSize!; feeBeingPaid = satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; @@ -528,7 +536,7 @@ mixin ElectrumXInterface on Bip39HDWallet { List utxosToUse, ) async { // return data - List signingData = []; + final List signingData = []; try { // Populating the addresses to check @@ -544,18 +552,6 @@ mixin ElectrumXInterface on Bip39HDWallet { ); } - final convertedNetwork = bitcoindart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ); - final root = await getRootHDNode(); for (final sd in signingData) { @@ -596,72 +592,7 @@ mixin ElectrumXInterface on Bip39HDWallet { "Failed to fetch signing data. Local db corrupt. Rescan wallet."); } - // final coinlib.Input input; - - final pubKey = keys.publicKey.data; - final bitcoindart.PaymentData data; - - switch (sd.derivePathType) { - case DerivePathType.bip44: - // input = coinlib.P2PKHInput( - // prevOut: coinlib.OutPoint.fromHex(sd.utxo.txid, sd.utxo.vout), - // publicKey: keys.publicKey, - // ); - - data = bitcoindart - .P2PKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; - break; - - case DerivePathType.bip49: - final p2wpkh = bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; - sd.redeemScript = p2wpkh.output; - data = bitcoindart - .P2SH( - data: bitcoindart.PaymentData(redeem: p2wpkh), - network: convertedNetwork, - ) - .data; - break; - - case DerivePathType.bip84: - // input = coinlib.P2WPKHInput( - // prevOut: coinlib.OutPoint.fromHex(sd.utxo.txid, sd.utxo.vout), - // publicKey: keys.publicKey, - // ); - data = bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; - break; - - default: - throw Exception("DerivePathType unsupported"); - } - - // sd.output = input.script!.compiled; - sd.output = data.output!; - sd.keyPair = bitcoindart.ECPair.fromPrivateKey( - keys.privateKey.data, - compressed: keys.privateKey.compressed, - network: convertedNetwork, - ); + sd.keyPair = keys; } return signingData; @@ -680,43 +611,84 @@ mixin ElectrumXInterface on Bip39HDWallet { Logging.instance .log("Starting buildTransaction ----------", level: LogLevel.Info); - // TODO: use coinlib - - final txb = bitcoindart.TransactionBuilder( - network: bitcoindart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ), - maximumFeeRate: maximumFeerate, - ); - const version = 1; // TODO possibly override this for certain coins? - txb.setVersion(version); - // temp tx data to show in gui while waiting for real data from server final List tempInputs = []; final List tempOutputs = []; + final List prevOuts = []; + + coinlib.Transaction clTx = coinlib.Transaction( + version: 1, // TODO: check if we can use 3 (as is default in coinlib) + inputs: [], + outputs: [], + ); + // Add transaction inputs for (var i = 0; i < utxoSigningData.length; i++) { final txid = utxoSigningData[i].utxo.txid; - txb.addInput( - txid, - utxoSigningData[i].utxo.vout, - null, - utxoSigningData[i].output!, - cryptoCurrency.networkParams.bech32Hrp, + + final hash = Uint8List.fromList( + txid.toUint8ListFromHex.reversed.toList(), ); + final prevOutpoint = coinlib.OutPoint( + hash, + utxoSigningData[i].utxo.vout, + ); + + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(utxoSigningData[i].utxo.value), + coinlib.Address.fromString( + utxoSigningData[i].utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + + final coinlib.Input input; + + switch (utxoSigningData[i].derivePathType) { + case DerivePathType.bip44: + case DerivePathType.bch44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: 0xffffffff - 1, + ); + + // TODO: fix this as it is (probably) wrong! + case DerivePathType.bip49: + throw Exception("TODO p2sh"); + // input = coinlib.P2SHMultisigInput( + // prevOut: prevOutpoint, + // program: coinlib.MultisigProgram.decompile( + // utxoSigningData[i].redeemScript!, + // ), + // sequence: 0xffffffff - 1, + // ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: 0xffffffff - 1, + ); + + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + + default: + throw UnsupportedError( + "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + ); + } + + clTx = clTx.addInput(input); + tempInputs.add( InputV2.isarCantDoRequiredInDefaultConstructor( - scriptSigHex: txb.inputs.first.script?.toHex, + scriptSigHex: input.scriptSig.toHex, scriptSigAsm: null, sequence: 0xffffffff - 1, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( @@ -737,12 +709,18 @@ mixin ElectrumXInterface on Bip39HDWallet { // Add transaction output for (var i = 0; i < txData.recipients!.length; i++) { - txb.addOutput( + final address = coinlib.Address.fromString( normalizeAddress(txData.recipients![i].address), - txData.recipients![i].amount.raw.toInt(), - cryptoCurrency.networkParams.bech32Hrp, + cryptoCurrency.networkParams, ); + final output = coinlib.Output.fromAddress( + txData.recipients![i].amount.raw, + address, + ); + + clTx = clTx.addOutput(output); + tempOutputs.add( OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "000000", @@ -765,12 +743,22 @@ mixin ElectrumXInterface on Bip39HDWallet { try { // Sign the transaction accordingly for (var i = 0; i < utxoSigningData.length; i++) { - txb.sign( - vin: i, - keyPair: utxoSigningData[i].keyPair!, - witnessValue: utxoSigningData[i].utxo.value, - redeemScript: utxoSigningData[i].redeemScript, - overridePrefix: cryptoCurrency.networkParams.bech32Hrp, + final value = BigInt.from(utxoSigningData[i].utxo.value); + coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; + + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot( + internalKey: utxoSigningData[i].keyPair!.publicKey, + ); + + key = taproot.tweakPrivateKey(key); + } + + clTx = clTx.sign( + inputN: i, + value: value, + key: key, + prevOuts: prevOuts, ); } } catch (e, s) { @@ -779,22 +767,20 @@ mixin ElectrumXInterface on Bip39HDWallet { rethrow; } - final builtTx = txb.build(cryptoCurrency.networkParams.bech32Hrp); - final vSize = builtTx.virtualSize(); - return txData.copyWith( - raw: builtTx.toHex(), - vSize: vSize, + raw: clTx.toHex(), + // dirty shortcut for peercoin's weirdness + vSize: this is PeercoinWallet ? clTx.size : clTx.vSize(), tempTx: TransactionV2( walletId: walletId, blockHash: null, - hash: builtTx.getId(), - txid: builtTx.getId(), + hash: clTx.hashHex, + txid: clTx.txid, height: null, timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(tempInputs), outputs: List.unmodifiable(tempOutputs), - version: version, + version: clTx.version, type: tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) && txData.paynymAccountLite == null @@ -808,24 +794,9 @@ mixin ElectrumXInterface on Bip39HDWallet { Future fetchChainHeight() async { try { - // Get the chain height service for the current coin. - ChainHeightService? service = ChainHeightServiceManager.getService( - cryptoCurrency.coin, + return await ClientManager.sharedInstance.getChainHeightFor( + cryptoCurrency, ); - - // ... or create a new one if it doesn't exist. - if (service == null) { - service = ChainHeightService(client: electrumAdapterClient); - ChainHeightServiceManager.add(service, cryptoCurrency.coin); - } - - // If the service hasn't been started, start it and fetch the chain height. - if (!service.started) { - return await service.fetchHeightAndStartListenForUpdates(); - } - - // Return the height as per the service if available or the cached height. - return service.height ?? info.cachedChainHeight; } catch (e, s) { Logging.instance.log( "Exception rethrown in fetchChainHeight\nError: $e\nStack trace: $s", @@ -865,7 +836,7 @@ mixin ElectrumXInterface on Bip39HDWallet { } } - Future getCurrentElectrumXNode() async { + Future _getCurrentElectrumXNode() async { final node = getCurrentNode(); return ElectrumXNode( @@ -877,7 +848,7 @@ mixin ElectrumXInterface on Bip39HDWallet { ); } - Future updateElectrumX({required ElectrumXNode newNode}) async { + Future updateElectrumX() async { final failovers = nodeService .failoverNodesFor(coin: cryptoCurrency.coin) .map((e) => ElectrumXNode( @@ -889,10 +860,10 @@ mixin ElectrumXInterface on Bip39HDWallet { )) .toList(); - final newNode = await getCurrentElectrumXNode(); + final newNode = await _getCurrentElectrumXNode(); try { - await electrumXClient.electrumAdapterClient?.close(); - } catch (e, s) { + await electrumXClient.closeAdapter(); + } catch (e) { if (e.toString().contains("initialized")) { // Ignore. This should happen every first time the wallet is opened. } else { @@ -904,50 +875,11 @@ mixin ElectrumXInterface on Bip39HDWallet { node: newNode, prefs: prefs, failovers: failovers, - coin: cryptoCurrency.coin, + cryptoCurrency: cryptoCurrency, ); - electrumAdapterChannel = await electrum_adapter.connect( - newNode.address, - port: newNode.port, - acceptUnverified: true, - useSSL: newNode.useSSL, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - if (electrumXClient.coin == Coin.firo || - electrumXClient.coin == Coin.firoTestNet) { - electrumAdapterClient = FiroElectrumClient( - electrumAdapterChannel, - newNode.address, - newNode.port, - newNode.useSSL, - Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - } else { - electrumAdapterClient = ElectrumClient( - electrumAdapterChannel, - newNode.address, - newNode.port, - newNode.useSSL, - Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null); - } electrumXCachedClient = CachedElectrumXClient.from( electrumXClient: electrumXClient, - electrumAdapterClient: electrumAdapterClient, - electrumAdapterUpdateCallback: updateClient, ); - // Replaced using electrum_adapters' SubscribableClient in fetchChainHeight. - // subscribableElectrumXClient = SubscribableElectrumXClient.from( - // node: newNode, - // prefs: prefs, - // failovers: failovers, - // ); - // await subscribableElectrumXClient.connect( - // host: newNode.address, port: newNode.port); } //============================================================================ @@ -958,7 +890,7 @@ mixin ElectrumXInterface on Bip39HDWallet { DerivePathType type, int chain, ) async { - List
addressArray = []; + final List
addressArray = []; int gapCounter = 0; int highestIndexWithHistory = 0; @@ -970,7 +902,7 @@ mixin ElectrumXInterface on Bip39HDWallet { "index: $index, \t GapCounter $chain ${type.name}: $gapCounter", level: LogLevel.Info); - List txCountCallArgs = []; + final List txCountCallArgs = []; for (int j = 0; j < txCountBatchSize; j++) { final derivePath = cryptoCurrency.constructDerivePath( @@ -1039,7 +971,7 @@ mixin ElectrumXInterface on Bip39HDWallet { DerivePathType type, int chain, ) async { - List
addressArray = []; + final List
addressArray = []; int gapCounter = 0; int index = 0; for (; @@ -1102,9 +1034,9 @@ mixin ElectrumXInterface on Bip39HDWallet { Iterable allAddresses, ) async { try { - List> allTxHashes = []; + final List> allTxHashes = []; - if (serverCanBatch) { + if (await serverCanBatch) { final Map>> batches = {}; final Map> batchIndexToAddressListMap = {}; const batchSizeMax = 100; @@ -1159,7 +1091,10 @@ mixin ElectrumXInterface on Bip39HDWallet { return allTxHashes; } catch (e, s) { - Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error); + Logging.instance.log( + "$runtimeType._fetchHistory: $e\n$s", + level: LogLevel.Error, + ); rethrow; } } @@ -1175,8 +1110,6 @@ mixin ElectrumXInterface on Bip39HDWallet { coin: cryptoCurrency.coin, ); - print("txn: $txn"); - final vout = jsonUTXO["tx_pos"] as int; final outputs = txn["vout"] as List; @@ -1241,15 +1174,7 @@ mixin ElectrumXInterface on Bip39HDWallet { @override Future updateNode() async { - final node = await getCurrentElectrumXNode(); - await updateElectrumX(newNode: node); - } - - Future updateClient() async { - Logging.instance.log("Updating electrum node and ElectrumAdapterClient.", - level: LogLevel.Info); - await updateNode(); - return electrumAdapterClient; + await updateElectrumX(); } FeeObject? _cachedFees; @@ -1452,9 +1377,11 @@ mixin ElectrumXInterface on Bip39HDWallet { level: LogLevel.Info, ); + final canBatch = await serverCanBatch; + for (final type in cryptoCurrency.supportedDerivationPathTypes) { receiveFutures.add( - serverCanBatch + canBatch ? checkGapsBatched( txCountBatchSize, root, @@ -1476,7 +1403,7 @@ mixin ElectrumXInterface on Bip39HDWallet { ); for (final type in cryptoCurrency.supportedDerivationPathTypes) { changeFutures.add( - serverCanBatch + canBatch ? checkGapsBatched( txCountBatchSize, root, @@ -1599,7 +1526,7 @@ mixin ElectrumXInterface on Bip39HDWallet { try { final fetchedUtxoList = >>[]; - if (serverCanBatch) { + if (await serverCanBatch) { final Map>> batchArgs = {}; const batchSizeMax = 10; int batchNumber = 0; @@ -1779,9 +1706,6 @@ mixin ElectrumXInterface on Bip39HDWallet { } } - @override - Future checkSaveInitialReceivingAddress() async {} - @override Future init() async { try { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart index 9e667c2b0..ce8c5064d 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart @@ -742,18 +742,20 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface { Future buildMintTransaction({required TxData txData}) async { final signingData = await fetchBuildTxData(txData.utxos!.toList()); - final txb = bitcoindart.TransactionBuilder( - network: bitcoindart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, + final convertedNetwork = bitcoindart.NetworkType( + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: bitcoindart.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); + + final txb = bitcoindart.TransactionBuilder( + network: convertedNetwork, ); txb.setVersion(2); @@ -763,11 +765,62 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface { int amount = 0; // Add transaction inputs for (var i = 0; i < signingData.length; i++) { + final pubKey = signingData[i].keyPair!.publicKey.data; + final bitcoindart.PaymentData? data; + + switch (signingData[i].derivePathType) { + case DerivePathType.bip44: + data = bitcoindart + .P2PKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip49: + final p2wpkh = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + data = bitcoindart + .P2SH( + data: bitcoindart.PaymentData(redeem: p2wpkh), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip84: + data = bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData( + pubkey: pubKey, + ), + network: convertedNetwork, + ) + .data; + break; + + case DerivePathType.bip86: + data = null; + break; + + default: + throw Exception("DerivePathType unsupported"); + } + txb.addInput( signingData[i].utxo.txid, signingData[i].utxo.vout, null, - signingData[i].output, + data!.output!, ); amount += signingData[i].utxo.value; } @@ -782,7 +835,11 @@ mixin LelantusInterface on Bip39HDWallet, ElectrumXInterface { for (var i = 0; i < signingData.length; i++) { txb.sign( vin: i, - keyPair: signingData[i].keyPair!, + keyPair: bitcoindart.ECPair.fromPrivateKey( + signingData[i].keyPair!.privateKey.data, + network: convertedNetwork, + compressed: signingData[i].keyPair!.privateKey.compressed, + ), witnessValue: signingData[i].utxo.value, ); } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index a2700fff6..202b22943 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -7,6 +7,7 @@ import 'package:bip47/bip47.dart'; import 'package:bitcoindart/bitcoindart.dart' as btc_dart; import 'package:bitcoindart/src/utils/constants/op.dart' as op; import 'package:bitcoindart/src/utils/script.dart' as bscript; +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; import 'package:pointycastle/digests/sha256.dart'; import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart'; @@ -20,6 +21,7 @@ import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/bip47_utils.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -290,7 +292,13 @@ mixin PaynymInterface return _cachedRootNode ??= await Bip32Utils.getBip32Root( (await getMnemonic()), (await getMnemonicPassphrase()), - networkType, + bip32.NetworkType( + wif: networkType.wif, + bip32: bip32.Bip32Type( + public: networkType.bip32.public, + private: networkType.bip32.private, + ), + ), ); } @@ -701,7 +709,7 @@ mixin PaynymInterface final myKeyPair = utxoSigningData.first.keyPair!; final S = SecretPoint( - myKeyPair.privateKey!, + myKeyPair.privateKey.data, targetPaymentCode.notificationPublicKey(), ); @@ -719,63 +727,146 @@ mixin PaynymInterface ]); // build a notification tx - final txb = btc_dart.TransactionBuilder(network: networkType); - txb.setVersion(1); - txb.addInput( - utxo.txid, - txPointIndex, - null, - utxoSigningData.first.output!, + final List prevOuts = []; + + coinlib.Transaction clTx = coinlib.Transaction( + version: 1, + inputs: [], + outputs: [], ); - // add rest of possible inputs - for (var i = 1; i < utxoSigningData.length; i++) { - final utxo = utxoSigningData[i].utxo; - txb.addInput( - utxo.txid, - utxo.vout, - null, - utxoSigningData[i].output!, + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + + final hash = Uint8List.fromList( + txid.toUint8ListFromHex.reversed.toList(), ); + + final prevOutpoint = coinlib.OutPoint( + hash, + utxoSigningData[i].utxo.vout, + ); + + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(utxoSigningData[i].utxo.value), + coinlib.Address.fromString( + utxoSigningData[i].utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + + final coinlib.Input input; + + switch (utxoSigningData[i].derivePathType) { + case DerivePathType.bip44: + case DerivePathType.bch44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: 0xffffffff - 1, + ); + + // TODO: fix this as it is (probably) wrong! (unlikely used in paynyms) + case DerivePathType.bip49: + throw Exception("TODO p2sh"); + // input = coinlib.P2SHMultisigInput( + // prevOut: prevOutpoint, + // program: coinlib.MultisigProgram.decompile( + // utxoSigningData[i].redeemScript!, + // ), + // sequence: 0xffffffff - 1, + // ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: 0xffffffff - 1, + ); + + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + + default: + throw UnsupportedError( + "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + ); + } + + clTx = clTx.addInput(input); } + final String notificationAddress = targetPaymentCode.notificationAddressP2PKH(); - txb.addOutput( - notificationAddress, - (overrideAmountForTesting ?? cryptoCurrency.dustLimitP2PKH.raw).toInt(), + final address = coinlib.Address.fromString( + normalizeAddress(notificationAddress), + cryptoCurrency.networkParams, + ); + + final output = coinlib.Output.fromAddress( + overrideAmountForTesting ?? cryptoCurrency.dustLimitP2PKH.raw, + address, + ); + + clTx = clTx.addOutput(output); + + clTx = clTx.addOutput( + coinlib.Output.fromScriptBytes( + BigInt.zero, + opReturnScript, + ), ); - txb.addOutput(opReturnScript, 0); // TODO: add possible change output and mark output as dangerous if (change > BigInt.zero) { // generate new change address if current change address has been used await checkChangeAddressForTransactions(); final String changeAddress = (await getCurrentChangeAddress())!.value; - txb.addOutput(changeAddress, change.toInt()); + + final output = coinlib.Output.fromAddress( + change, + coinlib.Address.fromString( + normalizeAddress(changeAddress), + cryptoCurrency.networkParams, + ), + ); + + clTx = clTx.addOutput(output); } - txb.sign( - vin: 0, - keyPair: myKeyPair, - witnessValue: utxo.value, - witnessScript: utxoSigningData.first.redeemScript, + clTx = clTx.sign( + inputN: 0, + value: BigInt.from(utxo.value), + key: myKeyPair.privateKey, + prevOuts: prevOuts, ); // sign rest of possible inputs - for (var i = 1; i < utxoSigningData.length; i++) { - txb.sign( - vin: i, - keyPair: utxoSigningData[i].keyPair!, - witnessValue: utxoSigningData[i].utxo.value, - witnessScript: utxoSigningData[i].redeemScript, + for (int i = 1; i < utxoSigningData.length; i++) { + final value = BigInt.from(utxoSigningData[i].utxo.value); + coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; + + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot( + internalKey: utxoSigningData[i].keyPair!.publicKey, + ); + + key = taproot.tweakPrivateKey(key); + } + + clTx = clTx.sign( + inputN: i, + value: value, + key: key, + prevOuts: prevOuts, ); } - final builtTx = txb.build(); - - return Tuple2(builtTx.toHex(), builtTx.virtualSize()); + return Tuple2(clTx.toHex(), clTx.vSize()); } catch (e, s) { Logging.instance.log( "_createNotificationTx(): $e\n$s", diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 74848182e..7aaa1d3ed 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2 import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; @@ -187,11 +188,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final transparentSumOut = (txData.recipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e); + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e, + ); // See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17 // and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17 @@ -202,16 +204,18 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { fractionDigits: cryptoCurrency.fractionDigits, )) { throw Exception( - "Spend to transparent address limit exceeded (10,000 Firo per transaction)."); + "Spend to transparent address limit exceeded (10,000 Firo per transaction).", + ); } final sparkSumOut = (txData.sparkRecipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e); + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e, + ); final txAmount = transparentSumOut + sparkSumOut; @@ -238,12 +242,14 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // prepare coin data for ffi final serializedCoins = coins - .map((e) => ( - serializedCoin: e.serializedCoinB64!, - serializedCoinContext: e.contextB64!, - groupId: e.groupId, - height: e.height!, - )) + .map( + (e) => ( + serializedCoin: e.serializedCoinB64!, + serializedCoinContext: e.contextB64!, + groupId: e.groupId, + height: e.height!, + ), + ) .toList(); final currentId = await electrumXClient.getSparkLatestCoinId(); @@ -253,6 +259,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { final set = await electrumXCachedClient.getSparkAnonymitySet( groupId: i.toString(), coin: info.coin, + useOnlyCacheIfNotEmpty: true, ); set["coinGroupID"] = i; setMaps.add(set); @@ -265,16 +272,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } final allAnonymitySets = setMaps - .map((e) => ( - setId: e["coinGroupID"] as int, - setHash: e["setHash"] as String, - set: (e["coins"] as List) - .map((e) => ( - serializedCoin: e[0] as String, - txHash: e[1] as String, - )) - .toList(), - )) + .map( + (e) => ( + setId: e["coinGroupID"] as int, + setHash: e["setHash"] as String, + set: (e["coins"] as List) + .map( + (e) => ( + serializedCoin: e[0] as String, + txHash: e[1] as String, + ), + ) + .toList(), + ), + ) .toList(); final root = await getRootHDNode(); @@ -436,27 +447,32 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { privateKeyHex: privateKey.toHex, index: kDefaultSparkIndex, recipients: txData.recipients - ?.map((e) => ( - address: e.address, - amount: e.amount.raw.toInt(), - subtractFeeFromAmount: isSendAll, - )) + ?.map( + (e) => ( + address: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + ), + ) .toList() ?? [], privateRecipients: txData.sparkRecipients - ?.map((e) => ( - sparkAddress: e.address, - amount: e.amount.raw.toInt(), - subtractFeeFromAmount: isSendAll, - memo: e.memo, - )) + ?.map( + (e) => ( + sparkAddress: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + memo: e.memo, + ), + ) .toList() ?? [], serializedCoins: serializedCoins, allAnonymitySets: allAnonymitySets, idAndBlockHashes: idAndBlockHashes .map( - (e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash))) + (e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash)), + ) .toList(), txHash: extractedTx.getHash(), ), @@ -503,16 +519,20 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { for (final usedCoin in spend.usedCoins) { try { - usedSparkCoins.add(coins - .firstWhere((e) => - usedCoin.height == e.height && - usedCoin.groupId == e.groupId && - base64Decode(e.serializedCoinB64!) - .toHex - .startsWith(base64Decode(usedCoin.serializedCoin).toHex)) - .copyWith( - isUsed: true, - )); + usedSparkCoins.add( + coins + .firstWhere( + (e) => + usedCoin.height == e.height && + usedCoin.groupId == e.groupId && + base64Decode(e.serializedCoinB64!).toHex.startsWith( + base64Decode(usedCoin.serializedCoin).toHex, + ), + ) + .copyWith( + isUsed: true, + ), + ); } catch (_) { throw Exception( "Unexpectedly did not find used spark coin. This should never happen.", @@ -575,15 +595,14 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { return await updateSentCachedTxData(txData: txData); } catch (e, s) { - Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", - level: LogLevel.Error); + Logging.instance.log( + "Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Error, + ); rethrow; } } - // TODO lots of room for performance improvements here. Should be similar to - // recoverSparkWallet but only fetch and check anonymity set data that we - // have not yet parsed. Future refreshSparkData() async { final sparkAddresses = await mainDB.isar.addresses .where() @@ -598,20 +617,14 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { try { final latestSparkCoinId = await electrumXClient.getSparkLatestCoinId(); - final blockHash = await _getCachedSparkBlockHash(); + final anonymitySetFuture = electrumXCachedClient.getSparkAnonymitySet( + groupId: latestSparkCoinId.toString(), + coin: info.coin, + useOnlyCacheIfNotEmpty: false, + ); - final anonymitySetFuture = blockHash == null - ? electrumXCachedClient.getSparkAnonymitySet( - groupId: latestSparkCoinId.toString(), - coin: info.coin, - ) - : electrumXClient.getSparkAnonymitySet( - coinGroupId: latestSparkCoinId.toString(), - startBlockHash: blockHash, - ); final spentCoinTagsFuture = - electrumXClient.getSparkUsedCoinsTags(startNumber: 0); - // electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin); + electrumXCachedClient.getSparkUsedCoinsTags(coin: info.coin); final futureResults = await Future.wait([ anonymitySetFuture, @@ -645,11 +658,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); myCoins.addAll(identifiedCoins); - - // update blockHash in cache - final String newBlockHash = - base64ToReverseHex(anonymitySet["blockHash"] as String); - await _setCachedSparkBlockHash(newBlockHash); } // check current coins @@ -671,8 +679,10 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // refresh spark balance await refreshSparkBalance(); } catch (e, s) { - // todo logging - + Logging.instance.log( + "$runtimeType $walletId ${info.name}: $e\n$s", + level: LogLevel.Error, + ); rethrow; } } @@ -694,9 +704,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { ); final spendable = Amount( rawValue: unusedCoins - .where((e) => - e.height != null && - e.height! + cryptoCurrency.minConfirms <= currentHeight) + .where( + (e) => + e.height != null && + e.height! + cryptoCurrency.minConfirms <= currentHeight, + ) .map((e) => e.value) .fold(BigInt.zero, (prev, e) => prev + e), fractionDigits: cryptoCurrency.fractionDigits, @@ -760,15 +772,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // update wallet spark coins in isar await _addOrUpdateSparkCoins(myCoins); - // update blockHash in cache - final String newBlockHash = anonymitySet["blockHash"] as String; - await _setCachedSparkBlockHash(newBlockHash); - // refresh spark balance await refreshSparkBalance(); } catch (e, s) { - // todo logging - + Logging.instance.log( + "$runtimeType $walletId ${info.name}: $e\n$s", + level: LogLevel.Error, + ); rethrow; } } @@ -801,7 +811,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { } // organise utxos - Map> utxosByAddress = {}; + final Map> utxosByAddress = {}; for (final utxo in availableUtxos) { utxosByAddress[utxo.address!] ??= []; utxosByAddress[utxo.address!]!.add(utxo); @@ -810,7 +820,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // setup some vars int nChangePosInOut = -1; - int nChangePosRequest = nChangePosInOut; + final int nChangePosRequest = nChangePosInOut; List outputs_ = outputs .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) .toList(); // deep copy @@ -933,11 +943,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // Generate dummy mint coins to save time final dummyRecipients = LibSpark.createSparkMintRecipients( outputs: singleTxOutputs - .map((e) => ( - sparkAddress: e.address, - value: e.value.toInt(), - memo: "", - )) + .map( + (e) => ( + sparkAddress: e.address, + value: e.value.toInt(), + memo: "", + ), + ) .toList(), serialContext: Uint8List(0), generate: false, @@ -951,11 +963,13 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { if (recipient.amount < cryptoCurrency.dustLimit.raw.toInt()) { throw Exception("Output amount too small"); } - vout.add(( - recipient.scriptPubKey, - recipient.amount, - singleTxOutputs[i].address, - )); + vout.add( + ( + recipient.scriptPubKey, + recipient.amount, + singleTxOutputs[i].address, + ), + ); } // Choose coins to use @@ -972,7 +986,7 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // priority stuff??? - BigInt nChange = nValueIn - nValueToSelect; + final BigInt nChange = nValueIn - nValueToSelect; if (nChange > BigInt.zero) { if (nChange < cryptoCurrency.dustLimit.raw) { nChangePosInOut = -1; @@ -1001,13 +1015,64 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { for (final sd in setCoins) { vin.add(sd); + final pubKey = sd.keyPair!.publicKey.data; + final btc.PaymentData? data; + + switch (sd.derivePathType) { + case DerivePathType.bip44: + data = btc + .P2PKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip49: + final p2wpkh = btc + .P2WPKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + data = btc + .P2SH( + data: btc.PaymentData(redeem: p2wpkh), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip84: + data = btc + .P2WPKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip86: + data = null; + break; + + default: + throw Exception("DerivePathType unsupported"); + } + // add to dummy tx dummyTxb.addInput( sd.utxo.txid, sd.utxo.vout, 0xffffffff - 1, // minus 1 is important. 0xffffffff on its own will burn funds - sd.output, + data!.output!, ); } @@ -1015,9 +1080,15 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { for (var i = 0; i < setCoins.length; i++) { dummyTxb.sign( vin: i, - keyPair: setCoins[i].keyPair!, + keyPair: btc.ECPair.fromPrivateKey( + setCoins[i].keyPair!.privateKey.data, + network: _bitcoinDartNetwork, + compressed: setCoins[i].keyPair!.privateKey.compressed, + ), witnessValue: setCoins[i].utxo.value, - redeemScript: setCoins[i].redeemScript, + + // maybe not needed here as this was originally copied from btc? We'll find out... + // redeemScript: setCoins[i].redeemScript, ); } @@ -1048,10 +1119,12 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // Generate real mint coins final serialContext = LibSpark.serializeMintContext( inputs: setCoins - .map((e) => ( - e.utxo.txid, - e.utxo.vout, - )) + .map( + (e) => ( + e.utxo.txid, + e.utxo.vout, + ), + ) .toList(), ); final recipients = LibSpark.createSparkMintRecipients( @@ -1068,7 +1141,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { generate: true, ); - int i = 0; for (int i = 0; i < recipients.length; i++) { final recipient = recipients[i]; final out = ( @@ -1114,12 +1186,63 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { txb.setVersion(txVersion); txb.setLockTime(lockTime); for (final input in vin) { + final pubKey = input.keyPair!.publicKey.data; + final btc.PaymentData? data; + + switch (input.derivePathType) { + case DerivePathType.bip44: + data = btc + .P2PKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip49: + final p2wpkh = btc + .P2WPKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + data = btc + .P2SH( + data: btc.PaymentData(redeem: p2wpkh), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip84: + data = btc + .P2WPKH( + data: btc.PaymentData( + pubkey: pubKey, + ), + network: _bitcoinDartNetwork, + ) + .data; + break; + + case DerivePathType.bip86: + data = null; + break; + + default: + throw Exception("DerivePathType unsupported"); + } + txb.addInput( input.utxo.txid, input.utxo.vout, 0xffffffff - 1, // minus 1 is important. 0xffffffff on its own will burn funds - input.output, + data!.output!, ); tempInputs.add( @@ -1158,9 +1281,11 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { .where() .walletIdEqualTo(walletId) .filter() - .valueEqualTo(addressOrScript is Uint8List - ? output.$3! - : addressOrScript as String) + .valueEqualTo( + addressOrScript is Uint8List + ? output.$3! + : addressOrScript as String, + ) .valueProperty() .findFirst()) != null, @@ -1172,9 +1297,15 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { for (var i = 0; i < vin.length; i++) { txb.sign( vin: i, - keyPair: vin[i].keyPair!, + keyPair: btc.ECPair.fromPrivateKey( + vin[i].keyPair!.privateKey.data, + network: _bitcoinDartNetwork, + compressed: vin[i].keyPair!.privateKey.compressed, + ), witnessValue: vin[i].utxo.value, - redeemScript: vin[i].redeemScript, + + // maybe not needed here as this was originally copied from btc? We'll find out... + // redeemScript: setCoins[i].redeemScript, ); } } catch (e, s) { @@ -1451,19 +1582,6 @@ mixin SparkInterface on Bip39HDWallet, ElectrumXInterface { // ====================== Private ============================================ - final _kSparkAnonSetCachedBlockHashKey = "SparkAnonSetCachedBlockHashKey"; - - Future _getCachedSparkBlockHash() async { - return info.otherData[_kSparkAnonSetCachedBlockHashKey] as String?; - } - - Future _setCachedSparkBlockHash(String blockHash) async { - await info.updateOtherData( - newEntries: {_kSparkAnonSetCachedBlockHashKey: blockHash}, - isar: mainDB.isar, - ); - } - Future _addOrUpdateSparkCoins(List coins) async { if (coins.isNotEmpty) { await mainDB.isar.writeTxn(() async { @@ -1528,42 +1646,38 @@ Future< String serializedCoinContext })> usedCoins, })> _createSparkSend( - ({ - String privateKeyHex, - int index, - List< - ({ - String address, - int amount, - bool subtractFeeFromAmount - })> recipients, - List< - ({ - String sparkAddress, - int amount, - bool subtractFeeFromAmount, - String memo - })> privateRecipients, - List< - ({ - String serializedCoin, - String serializedCoinContext, - int groupId, - int height, - })> serializedCoins, - List< - ({ - int setId, - String setHash, - List<({String serializedCoin, String txHash})> set - })> allAnonymitySets, - List< - ({ - int setId, - Uint8List blockHash, - })> idAndBlockHashes, - Uint8List txHash, - }) args) async { + ({ + String privateKeyHex, + int index, + List<({String address, int amount, bool subtractFeeFromAmount})> recipients, + List< + ({ + String sparkAddress, + int amount, + bool subtractFeeFromAmount, + String memo + })> privateRecipients, + List< + ({ + String serializedCoin, + String serializedCoinContext, + int groupId, + int height, + })> serializedCoins, + List< + ({ + int setId, + String setHash, + List<({String serializedCoin, String txHash})> set + })> allAnonymitySets, + List< + ({ + int setId, + Uint8List blockHash, + })> idAndBlockHashes, + Uint8List txHash, + }) args, +) async { final spend = LibSpark.createSparkSendTransaction( privateKeyHex: args.privateKeyHex, index: args.index, @@ -1580,14 +1694,15 @@ Future< /// Top level function which should be called wrapped in [compute] Future> _identifyCoins( - ({ - List anonymitySetCoins, - int groupId, - Set spentCoinTags, - Set privateKeyHexSet, - String walletId, - bool isTestNet, - }) args) async { + ({ + List anonymitySetCoins, + int groupId, + Set spentCoinTags, + Set privateKeyHexSet, + String walletId, + bool isTestNet, + }) args, +) async { final List myCoins = []; for (final privateKeyHex in args.privateKeyHexSet) { diff --git a/lib/widgets/custom_buttons/checkbox_text_button.dart b/lib/widgets/custom_buttons/checkbox_text_button.dart new file mode 100644 index 000000000..8ad254ae9 --- /dev/null +++ b/lib/widgets/custom_buttons/checkbox_text_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; + +class CheckboxTextButton extends StatefulWidget { + const CheckboxTextButton({super.key, required this.label, this.onChanged}); + + final String label; + final void Function(bool)? onChanged; + + @override + State createState() => _CheckboxTextButtonState(); +} + +class _CheckboxTextButtonState extends State { + bool _value = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + setState(() { + _value = !_value; + }); + widget.onChanged?.call(_value); + }, + child: Container( + color: Colors.transparent, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 20, + height: 26, + child: IgnorePointer( + child: Checkbox( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _value, + onChanged: (_) {}, + ), + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + widget.label, + style: STextStyles.w500_14(context), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/custom_buttons/frost_qr_dialog_button.dart b/lib/widgets/custom_buttons/frost_qr_dialog_button.dart new file mode 100644 index 000000000..0b63f097e --- /dev/null +++ b/lib/widgets/custom_buttons/frost_qr_dialog_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_step_qr_dialog.dart'; + +class FrostQrDialogPopupButton extends ConsumerWidget { + const FrostQrDialogPopupButton({super.key, required this.data}); + + final String data; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SecondaryButton( + label: "View QR code", + icon: SvgPicture.asset( + Assets.svg.qrcode, + colorFilter: ColorFilter.mode( + Theme.of(context).extension()!.buttonTextSecondary, + BlendMode.srcIn, + ), + ), + onPressed: () async { + await showDialog( + context: context, + builder: (_) => FrostStepQrDialog( + myName: ref.read(pFrostMyName)!, + title: "Step " + "${ref.read(pFrostCreateCurrentStep)}" + " of " + "${ref.read(pFrostScaffoldArgs)!.stepRoutes.length}" + " - ${ref.read(pFrostScaffoldArgs)!.stepRoutes[ref.watch(pFrostCreateCurrentStep) - 1].title}", + data: data, + ), + ); + }, + ); + } +} diff --git a/lib/widgets/custom_buttons/simple_edit_button.dart b/lib/widgets/custom_buttons/simple_edit_button.dart index 26b7042b9..99d470fe3 100644 --- a/lib/widgets/custom_buttons/simple_edit_button.dart +++ b/lib/widgets/custom_buttons/simple_edit_button.dart @@ -15,29 +15,31 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/icon_widgets/pencil_icon.dart'; import 'package:tuple/tuple.dart'; -import '../desktop/desktop_dialog.dart'; -import '../icon_widgets/pencil_icon.dart'; - class SimpleEditButton extends StatelessWidget { const SimpleEditButton({ - Key? key, + super.key, this.editValue, this.editLabel, + this.overrideTitle, + this.disableIcon = false, this.onValueChanged, this.onPressedOverride, - }) : assert( + }) : assert( (editLabel != null && editValue != null && onValueChanged != null) || (editLabel == null && editValue == null && onValueChanged == null && onPressedOverride != null), - ), - super(key: key); + ); final String? editValue; final String? editLabel; + final String? overrideTitle; + final bool disableIcon; final void Function(String)? onValueChanged; final VoidCallback? onPressedOverride; @@ -101,17 +103,20 @@ class SimpleEditButton extends StatelessWidget { }, child: Row( children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context).extension()!.infoItemIcons, - ), - const SizedBox( - width: 4, - ), + if (!disableIcon) + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: + Theme.of(context).extension()!.infoItemIcons, + ), + if (!disableIcon) + const SizedBox( + width: 4, + ), Text( - "Edit", + overrideTitle ?? "Edit", style: STextStyles.link2(context), ), ], diff --git a/lib/widgets/date_picker/date_picker.dart b/lib/widgets/date_picker/date_picker.dart new file mode 100644 index 000000000..054f89ee5 --- /dev/null +++ b/lib/widgets/date_picker/date_picker.dart @@ -0,0 +1,108 @@ +import 'dart:math'; + +import 'package:calendar_date_picker2/calendar_date_picker2.dart'; +import 'package:flutter/material.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +part 'sw_date_picker.dart'; + +Future showSWDatePicker(BuildContext context) async { + final Size size; + if (Util.isDesktop) { + size = const Size(450, 450); + } else { + final _size = MediaQuery.of(context).size; + size = Size( + _size.width - 32, + _size.height >= 550 ? 450 : _size.height - 32, + ); + } + print("====================================="); + print(size); + + final now = DateTime.now(); + + final date = await _showDatePickerDialog( + context: context, + value: [now], + dialogSize: size, + config: CalendarDatePicker2WithActionButtonsConfig( + firstDate: DateTime(2007), + lastDate: now, + currentDate: now, + buttonPadding: const EdgeInsets.only( + right: 16, + ), + centerAlignModePicker: true, + selectedDayHighlightColor: + Theme.of(context).extension()!.accentColorDark, + daySplashColor: Theme.of(context) + .extension()! + .accentColorDark + .withOpacity(0.6), + ), + ); + return date?.first; +} + +Future?> _showDatePickerDialog({ + required BuildContext context, + required CalendarDatePicker2WithActionButtonsConfig config, + required Size dialogSize, + List value = const [], + bool useRootNavigator = true, + bool barrierDismissible = true, + Color? barrierColor = Colors.black54, + bool useSafeArea = true, + RouteSettings? routeSettings, + String? barrierLabel, + TransitionBuilder? builder, +}) { + final dialog = Dialog( + insetPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + backgroundColor: Theme.of(context).extension()!.popupBG, + surfaceTintColor: Colors.transparent, + shadowColor: Colors.transparent, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius * 2, + ), + ), + clipBehavior: Clip.antiAlias, + child: SizedBox( + width: dialogSize.width, + height: max(dialogSize.height, 410), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _SWDatePicker( + value: value, + config: config.copyWith(openedFromDialog: true), + ), + ], + ), + ), + ); + + return showDialog>( + context: context, + useRootNavigator: useRootNavigator, + routeSettings: routeSettings, + builder: (BuildContext context) { + return builder == null ? dialog : builder(context, dialog); + }, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + barrierLabel: barrierLabel, + useSafeArea: useSafeArea, + ); +} diff --git a/lib/widgets/date_picker/sw_date_picker.dart b/lib/widgets/date_picker/sw_date_picker.dart new file mode 100644 index 000000000..665787139 --- /dev/null +++ b/lib/widgets/date_picker/sw_date_picker.dart @@ -0,0 +1,184 @@ +part of 'date_picker.dart'; + +class _SWDatePicker extends StatefulWidget { + const _SWDatePicker({ + super.key, + required this.value, + required this.config, + this.onValueChanged, + this.onDisplayedMonthChanged, + this.onCancelTapped, + this.onOkTapped, + }); + final List value; + + /// Called when the user taps 'OK' button + final ValueChanged>? onValueChanged; + + /// Called when the user navigates to a new month/year in the picker. + final ValueChanged? onDisplayedMonthChanged; + + /// The calendar configurations including action buttons + final CalendarDatePicker2WithActionButtonsConfig config; + + /// The callback when cancel button is tapped + final Function? onCancelTapped; + + /// The callback when ok button is tapped + final Function? onOkTapped; + @override + State<_SWDatePicker> createState() => _SWDatePickerState(); +} + +class _SWDatePickerState extends State<_SWDatePicker> { + List _values = []; + List _editCache = []; + + @override + void initState() { + _values = widget.value; + _editCache = widget.value; + super.initState(); + } + + @override + void didUpdateWidget(covariant _SWDatePicker oldWidget) { + var isValueSame = oldWidget.value.length == widget.value.length; + + if (isValueSame) { + for (int i = 0; i < oldWidget.value.length; i++) { + final isSame = + (oldWidget.value[i] == null && widget.value[i] == null) || + DateUtils.isSameDay(oldWidget.value[i], widget.value[i]); + if (!isSame) { + isValueSame = false; + break; + } + } + } + + if (!isValueSame) { + _values = widget.value; + _editCache = widget.value; + } + + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + hoverColor: Colors.transparent, + highlightColor: Colors.transparent, + focusColor: Colors.transparent, + colorScheme: Theme.of(context).colorScheme.copyWith( + background: Theme.of(context).extension()!.popupBG, + onBackground: + Theme.of(context).extension()!.accentColorDark, + surface: Theme.of(context).extension()!.popupBG, + surfaceVariant: + Theme.of(context).extension()!.popupBG, + onSurface: + Theme.of(context).extension()!.accentColorDark, + onSurfaceVariant: + Theme.of(context).extension()!.accentColorDark, + surfaceTint: Colors.transparent, + shadow: Colors.transparent, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MediaQuery.removePadding( + context: context, + child: CalendarDatePicker2( + value: [..._editCache], + config: widget.config, + onValueChanged: (values) => _editCache = values, + onDisplayedMonthChanged: widget.onDisplayedMonthChanged, + ), + ), + SizedBox(height: widget.config.gapBetweenCalendarAndButtons ?? 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!Util.isDesktop) + SizedBox( + width: widget.config.buttonPadding?.right ?? 0, + ), + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: Padding( + padding: widget.config.buttonPadding ?? EdgeInsets.zero, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => SizedBox( + width: 140, + child: child, + ), + child: SecondaryButton( + label: "Cancel", + buttonHeight: Util.isDesktop ? ButtonHeight.m : null, + onPressed: () { + setState( + () { + _editCache = _values; + widget.onCancelTapped?.call(); + if ((widget.config.openedFromDialog ?? false) && + (widget.config.closeDialogOnCancelTapped ?? + true)) { + Navigator.pop(context); + } + }, + ); + }, + ), + ), + ), + ), + if ((widget.config.gapBetweenCalendarAndButtons ?? 0) > 0) + SizedBox(width: widget.config.gapBetweenCalendarAndButtons), + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: Padding( + padding: widget.config.buttonPadding ?? EdgeInsets.zero, + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => SizedBox( + width: 140, + child: child, + ), + child: PrimaryButton( + buttonHeight: Util.isDesktop ? ButtonHeight.m : null, + label: "Ok", + onPressed: () { + setState( + () { + _values = _editCache; + widget.onValueChanged?.call(_values); + widget.onOkTapped?.call(); + if ((widget.config.openedFromDialog ?? false) && + (widget.config.closeDialogOnOkTapped ?? true)) { + Navigator.pop(context, _values); + } + }, + ); + }, + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/detail_item.dart b/lib/widgets/detail_item.dart new file mode 100644 index 000000000..ee3694d0a --- /dev/null +++ b/lib/widgets/detail_item.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DetailItem extends StatelessWidget { + const DetailItem({ + super.key, + required this.title, + required this.detail, + this.button, + this.overrideDetailTextColor, + this.showEmptyDetail = true, + this.horizontal = false, + this.disableSelectableText = false, + }); + + final String title; + final String detail; + final Widget? button; + final bool showEmptyDetail; + final bool horizontal; + final bool disableSelectableText; + final Color? overrideDetailTextColor; + + @override + Widget build(BuildContext context) { + final TextStyle detailStyle; + if (overrideDetailTextColor != null) { + detailStyle = STextStyles.w500_14(context).copyWith( + color: overrideDetailTextColor, + ); + } else { + detailStyle = STextStyles.w500_14(context); + } + + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => RoundedWhiteContainer( + child: child, + ), + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + child: horizontal + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + disableSelectableText + ? Text( + title, + style: STextStyles.itemSubtitle(context), + ) + : SelectableText( + title, + style: STextStyles.itemSubtitle(context), + ), + disableSelectableText + ? Text( + detail, + style: detailStyle, + ) + : SelectableText( + detail, + style: detailStyle, + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + disableSelectableText + ? Text( + title, + style: STextStyles.itemSubtitle(context), + ) + : SelectableText( + title, + style: STextStyles.itemSubtitle(context), + ), + button ?? Container(), + ], + ), + const SizedBox( + height: 5, + ), + detail.isEmpty && showEmptyDetail + ? disableSelectableText + ? Text( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ) + : SelectableText( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ) + : disableSelectableText + ? Text( + detail, + style: detailStyle, + ) + : SelectableText( + detail, + style: detailStyle, + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/dialogs/basic_dialog.dart b/lib/widgets/dialogs/basic_dialog.dart index 271d82049..d2c5e39eb 100644 --- a/lib/widgets/dialogs/basic_dialog.dart +++ b/lib/widgets/dialogs/basic_dialog.dart @@ -26,6 +26,7 @@ class BasicDialog extends StatelessWidget { this.desktopHeight = 474, this.desktopWidth = 641, this.canPopWithBackButton = false, + this.flex = false, }) : super(key: key); final Widget? leftButton; @@ -41,6 +42,8 @@ class BasicDialog extends StatelessWidget { final bool canPopWithBackButton; + final bool flex; + @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; @@ -64,6 +67,10 @@ class BasicDialog extends StatelessWidget { ], ), ), + if (flex) + const Spacer( + flex: 2, + ), if (message != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 32), @@ -72,6 +79,10 @@ class BasicDialog extends StatelessWidget { style: STextStyles.desktopTextSmall(context), ), ), + if (flex) + const Spacer( + flex: 3, + ), if (leftButton != null || rightButton != null) const SizedBox( height: 32, diff --git a/lib/widgets/dialogs/frost/frost_error_dialog.dart b/lib/widgets/dialogs/frost/frost_error_dialog.dart new file mode 100644 index 000000000..8b1e56e15 --- /dev/null +++ b/lib/widgets/dialogs/frost/frost_error_dialog.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FrostErrorDialog extends ConsumerWidget { + const FrostErrorDialog({ + super.key, + required this.title, + this.icon, + this.message, + }); + + final String title; + final Widget? icon; + final String? message; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SafeArea( + child: StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + title, + style: STextStyles.pageTitleH2(context), + ), + ), + icon != null ? icon! : Container(), + ], + ), + if (message != null) + const SizedBox( + height: 8, + ), + if (message != null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + message!, + style: STextStyles.smallMed14(context), + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Process must be restarted", + style: STextStyles.smallMed14(context), + ), + ], + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + const Spacer(), + const SizedBox( + width: 8, + ), + PrimaryButton( + label: "Ok", + onPressed: () { + ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true; + ref.read(pFrostScaffoldArgs)!.parentNav.popUntil( + ModalRoute.withName( + ref.read(pFrostScaffoldArgs)!.callerRouteName, + ), + ); + }, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/dialogs/frost/frost_step_explanation_dialog.dart b/lib/widgets/dialogs/frost/frost_step_explanation_dialog.dart new file mode 100644 index 000000000..102bab1e5 --- /dev/null +++ b/lib/widgets/dialogs/frost/frost_step_explanation_dialog.dart @@ -0,0 +1,65 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FrostStepExplanationDialog extends StatelessWidget { + final String title; + final String body; + const FrostStepExplanationDialog({super.key, required this.title, required this.body}); + + @override + Widget build(BuildContext context) { + return StackDialogBase( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + height: 12, + ), + Text( + body, + style: STextStyles.baseXS(context), + ), + ], + ), + ), + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: SecondaryButton( + label: "Close", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart b/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart new file mode 100644 index 000000000..2dd1ace26 --- /dev/null +++ b/lib/widgets/dialogs/frost/frost_step_qr_dialog.dart @@ -0,0 +1,209 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/dialogs/simple_mobile_dialog.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostStepQrDialog extends StatefulWidget { + const FrostStepQrDialog({ + super.key, + required this.myName, + required this.title, + required this.data, + }); + + final String myName; + final String title; + final String data; + + @override + State createState() => _FrostStepQrDialogState(); +} + +class _FrostStepQrDialogState extends State { + final _qrKey = GlobalKey(); + + Future _capturePng(bool shouldSaveInsteadOfShare) async { + try { + final boundary = + _qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary; + final image = await boundary.toImage(); + final byteData = await image.toByteData(format: ImageByteFormat.png); + final pngBytes = byteData!.buffer.asUint8List(); + + if (shouldSaveInsteadOfShare) { + if (Util.isDesktop) { + final dir = Directory("${Platform.environment['HOME']}"); + if (!dir.existsSync()) { + throw Exception( + "Home dir not found while trying to open filepicker on QR image save"); + } + final path = await FilePicker.platform.saveFile( + fileName: "qrcode.png", + initialDirectory: dir.path, + ); + + if (path != null && context.mounted) { + final file = File(path); + if (file.existsSync()) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$path already exists!", + context: context, + ), + ); + } else { + await file.writeAsBytes(pngBytes); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "$path saved!", + context: context, + ), + ); + } + } + } else { + // await DocumentFileSavePlus.saveFile( + // pngBytes, + // "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png", + // "image/png"); + } + } else { + final tempDir = await getTemporaryDirectory(); + final file = await File("${tempDir.path}/qrcode.png").create(); + await file.writeAsBytes(pngBytes); + + await Share.shareFiles(["${tempDir.path}/qrcode.png"], + text: "Receive URI QR Code"); + } + } catch (e) { + //todo: comeback to this + debugPrint(e.toString()); + } + } + + @override + Widget build(BuildContext context) { + return SimpleMobileDialog( + showCloseButton: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RepaintBoundary( + key: _qrKey, + child: RoundedWhiteContainer( + boxShadow: [ + Theme.of(context).extension()!.standardBoxShadow + ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.myName, + style: STextStyles.w600_16(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + const SizedBox(height: 8), + Text( + widget.title, + style: STextStyles.w600_12(context), + ), + const SizedBox(height: 8), + RoundedContainer( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + radiusMultiplier: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ConditionalParent( + condition: Util.isDesktop, + builder: (child) => ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 360, + ), + child: child, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: AspectRatio( + aspectRatio: 1, + child: QrImageView( + data: widget.data, + padding: EdgeInsets.zero, + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + ), + ), + const SizedBox(height: 12), + SelectableText( + widget.data, + style: STextStyles.w500_10(context), + ), + ], + ), + ), + ], + ), + ), + ), + if (!Util.isDesktop) + const SizedBox( + height: 16, + ), + if (!Util.isDesktop) + Row( + children: [ + const Spacer(), + const SizedBox(width: 16), + Expanded( + child: SecondaryButton( + label: "Share", + icon: SvgPicture.asset( + Assets.svg.share, + width: 14, + height: 14, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + await _capturePng(false); + }, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/dialogs/simple_mobile_dialog.dart b/lib/widgets/dialogs/simple_mobile_dialog.dart new file mode 100644 index 000000000..1e07e22ae --- /dev/null +++ b/lib/widgets/dialogs/simple_mobile_dialog.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class SimpleMobileDialog extends StatelessWidget { + const SimpleMobileDialog({ + super.key, + required this.child, + this.showCloseButton = true, + this.padding, + }); + + final Widget child; + final bool showCloseButton; + final EdgeInsets? padding; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Padding( + padding: const EdgeInsets.all(16), + child: Material( + borderRadius: BorderRadius.circular( + 20, + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: Padding( + padding: padding ?? const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: child, + ), + ), + if (showCloseButton) + const SizedBox( + height: 16, + ), + if (showCloseButton) + Row( + children: [ + const Spacer(), + const SizedBox( + width: 16, + ), + Expanded( + child: SecondaryButton( + label: "Close", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/dialogs/tor_warning_dialog.dart b/lib/widgets/dialogs/tor_warning_dialog.dart new file mode 100644 index 000000000..d4bd7dc81 --- /dev/null +++ b/lib/widgets/dialogs/tor_warning_dialog.dart @@ -0,0 +1,44 @@ +import 'package:flutter/cupertino.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/dialogs/basic_dialog.dart'; + +class TorWarningDialog extends StatelessWidget { + final Coin coin; + final VoidCallback? onContinue; + final VoidCallback? onCancel; + + TorWarningDialog({ + Key? key, + required this.coin, + this.onContinue, + this.onCancel, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BasicDialog( + title: "Warning! Tor not supported.", + message: "${coin.prettyName} is not compatible with Tor. " + "Continuing will leak your IP address." + "\n\nAre you sure you want to continue?", + // A PrimaryButton widget: + leftButton: PrimaryButton( + label: "Cancel", + onPressed: () { + onCancel?.call(); + Navigator.of(context).pop(false); + }, + ), + rightButton: SecondaryButton( + label: "Continue", + onPressed: () { + onContinue?.call(); + Navigator.of(context).pop(true); + }, + ), + flex: true, + ); + } +} diff --git a/lib/widgets/fee_slider.dart b/lib/widgets/fee_slider.dart index d125988f3..64e3af12b 100644 --- a/lib/widgets/fee_slider.dart +++ b/lib/widgets/fee_slider.dart @@ -9,9 +9,11 @@ class FeeSlider extends StatefulWidget { super.key, required this.onSatVByteChanged, required this.coin, + this.showWU = false, }); final Coin coin; + final bool showWU; final void Function(int) onSatVByteChanged; @override @@ -34,7 +36,7 @@ class _FeeSliderState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "sat/vByte", + widget.showWU ? "sat/WU" : "sat/vByte", style: STextStyles.smallMed12(context), ), Text( diff --git a/lib/widgets/frost_mascot.dart b/lib/widgets/frost_mascot.dart new file mode 100644 index 000000000..17743efcc --- /dev/null +++ b/lib/widgets/frost_mascot.dart @@ -0,0 +1,51 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/widgets/dialogs/frost/frost_step_explanation_dialog.dart'; + +class FrostMascot extends StatelessWidget { + final String title; + final String body; + const FrostMascot({ + super.key, + this.onPressed, + required this.title, + required this.body, + }); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + right: 24, + ), + child: GestureDetector( + onTap: () async { + await showDialog( + context: context, + builder: (context) => FrostStepExplanationDialog( + title: title, + body: body, + ), + ); + }, + child: Image( + image: AssetImage( + Assets.png.mascot, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/frost_scaffold.dart b/lib/widgets/frost_scaffold.dart new file mode 100644 index 000000000..e5d83e088 --- /dev/null +++ b/lib/widgets/frost_scaffold.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/frost_route_generator.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/progress_bar.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FrostStepScaffold extends ConsumerStatefulWidget { + const FrostStepScaffold({super.key}); + + static const String routeName = "/frostStepScaffold"; + + @override + ConsumerState createState() => _FrostScaffoldState(); +} + +class _FrostScaffoldState extends ConsumerState { + static const _titleTextSize = 18.0; + final _navigatorKey = GlobalKey(); + + late final List _routes; + + bool _requestPopLock = false; + + String get _message { + switch (ref.read(pFrostScaffoldArgs)!.frostInterruptionDialogType) { + case FrostInterruptionDialogType.walletCreation: + return "wallet creation"; + case FrostInterruptionDialogType.resharing: + return "resharing"; + case FrostInterruptionDialogType.transactionCreation: + return "transaction signing"; + } + } + + Future _requestPop(BuildContext context) async { + if (_requestPopLock || + (Util.isDesktop && ref.read(pFrostScaffoldCanPopDesktop))) { + return; + } + _requestPopLock = true; + + final resultFuture = showDialog( + context: context, + barrierDismissible: false, + builder: (context) => StackDialog( + title: "Cancel $_message process", + message: "Are you sure you want to cancel the $_message process?", + leftButton: SecondaryButton( + label: "No", + onPressed: () { + // pop dialog + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop("no"); + }, + ), + rightButton: PrimaryButton( + label: "Yes", + onPressed: () { + // pop dialog + Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop("yes"); + }, + ), + ), + ); + + // make sure to at least delay some time otherwise flutter pops back more than a single route lol... + final minTimeFuture = + Future.delayed(const Duration(milliseconds: 200)); + + final result = await Future.wait([resultFuture, minTimeFuture]); + + if (context.mounted && result[0] == "yes") { + Navigator.of(context).pop(); + ref.read(pFrostScaffoldArgs.state).state = null; + } + + _requestPopLock = false; + } + + @override + void initState() { + _routes = ref.read(pFrostScaffoldArgs)!.stepRoutes; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: Util.isDesktop && ref.watch(pFrostScaffoldCanPopDesktop), + onPopInvoked: (_) => _requestPop(context), + child: Material( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => child, + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + body: SafeArea( + child: child, + ), + ), + ), + child: Column( + children: [ + // header + SizedBox( + height: 56, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Row( + children: [ + Text( + "${ref.watch(pFrostCreateCurrentStep)} / ${_routes.length}", + style: STextStyles.navBarTitle(context).copyWith( + fontSize: _titleTextSize, + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Text( + _routes[ref.watch(pFrostCreateCurrentStep) - 1] + .title, + style: STextStyles.navBarTitle(context).copyWith( + fontSize: _titleTextSize, + ), + ), + ), + const SizedBox( + width: 10, + ), + CustomTextButton( + text: "Exit", + textSize: _titleTextSize, + onTap: () => _requestPop(context), + ), + ], + ), + ), + ), + LayoutBuilder( + builder: (subContext, constraints) => ProgressBar( + width: constraints.maxWidth, + height: 3, + fillColor: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + backgroundColor: Theme.of(context) + .extension()! + .customTextButtonEnabledText + .withOpacity(0.1), + percent: + ref.watch(pFrostCreateCurrentStep) / _routes.length, + ), + ), + Expanded( + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: SizedBox( + width: 500, + child: child, + ), + ) + ], + ), + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }, + ), + child: Navigator( + key: _navigatorKey, + initialRoute: _routes[0].routeName, + onGenerateRoute: FrostRouteGenerator.generateRoute, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/frost_step_user_steps.dart b/lib/widgets/frost_step_user_steps.dart new file mode 100644 index 000000000..729264344 --- /dev/null +++ b/lib/widgets/frost_step_user_steps.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class FrostStepUserSteps extends StatelessWidget { + const FrostStepUserSteps({super.key, required this.userSteps}); + + final List userSteps; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + children: [ + for (int i = 0; i < userSteps.length; i++) + ConditionalParent( + condition: i > 0, + builder: (child) => Padding( + padding: const EdgeInsets.only(top: 4), + child: child, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${i + 1}.", + style: STextStyles.w500_12(context), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: Text( + userSteps[i], + style: STextStyles.w500_12(context), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index bc7233065..a6ac18302 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -13,15 +13,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:solana/solana.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart'; import 'package:stackwallet/providers/global/active_wallet_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -169,17 +171,18 @@ class _NodeCardState extends ConsumerState { case Coin.namecoin: case Coin.bitcoincashTestnet: case Coin.eCash: - final client = ElectrumXClient( - host: node.host, - port: node.port, - useSSL: node.useSSL, - failovers: [], - prefs: ref.read(prefsChangeNotifierProvider), - coin: widget.coin, - ); - + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + case Coin.peercoin: + case Coin.peercoinTestNet: try { - testPassed = await client.ping(); + testPassed = await checkElectrumServer( + host: node.host, + port: node.port, + useSSL: node.useSSL, + overridePrefs: ref.read(prefsChangeNotifierProvider), + overrideTorService: ref.read(pTorService), + ); } catch (_) { testPassed = false; } @@ -213,6 +216,20 @@ class _NodeCardState extends ConsumerState { testPassed = false; } break; + + case Coin.solana: + try { + RpcClient rpcClient; + if (node.host.startsWith("http") || node.host.startsWith("https")) { + rpcClient = RpcClient("${node.host}:${node.port}"); + } else { + rpcClient = RpcClient("http://${node.host}:${node.port}"); + } + await rpcClient.getEpochInfo().then((value) => testPassed = true); + } catch (_) { + testPassed = false; + } + break; } if (testPassed) { diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index 0a2e5ac4c..31bdec456 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -13,7 +13,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +import 'package:solana/solana.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; @@ -23,6 +23,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -151,18 +152,18 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.namecoin: case Coin.bitcoincashTestnet: case Coin.eCash: - final client = ElectrumXClient( - host: node.host, - port: node.port, - useSSL: node.useSSL, - failovers: [], - prefs: ref.read(prefsChangeNotifierProvider), - torService: ref.read(pTorService), - coin: coin, - ); - + case Coin.bitcoinFrost: + case Coin.bitcoinFrostTestNet: + case Coin.peercoin: + case Coin.peercoinTestNet: try { - testPassed = await client.ping(); + testPassed = await checkElectrumServer( + host: node.host, + port: node.port, + useSSL: node.useSSL, + overridePrefs: ref.read(prefsChangeNotifierProvider), + overrideTorService: ref.read(pTorService), + ); } catch (_) { testPassed = false; } @@ -184,6 +185,20 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.stellarTestnet: throw UnimplementedError(); //TODO: check network/node + + case Coin.solana: + try { + RpcClient rpcClient; + if (node.host.startsWith("http") || node.host.startsWith("https")) { + rpcClient = RpcClient("${node.host}:${node.port}"); + } else { + rpcClient = RpcClient("http://${node.host}:${node.port}"); + } + await rpcClient.getEpochInfo().then((value) => testPassed = true); + } catch (_) { + testPassed = false; + } + break; } if (testPassed) { diff --git a/lib/widgets/rounded_date_picker/LICENSE b/lib/widgets/rounded_date_picker/LICENSE deleted file mode 100644 index 58665fbd2..000000000 --- a/lib/widgets/rounded_date_picker/LICENSE +++ /dev/null @@ -1,5 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at: - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart deleted file mode 100644 index 3c8ff39bb..000000000 --- a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart +++ /dev/null @@ -1,342 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'package:flutter/material.dart'; -import 'package:flutter/semantics.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; -import 'package:flutter_rounded_date_picker/src/flutter_rounded_button_action.dart'; -import 'package:flutter_rounded_date_picker/src/material_rounded_date_picker_style.dart'; -import 'package:flutter_rounded_date_picker/src/material_rounded_year_picker_style.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_date_picker_header.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_day_picker.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_month_picker.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_year_picker.dart'; -import 'package:stackwallet/utilities/util.dart'; - -/// -/// This file uses code taken from https://github.com/benznest/flutter_rounded_date_picker -/// - -class FlutterRoundedDatePickerDialog extends StatefulWidget { - const FlutterRoundedDatePickerDialog( - {Key? key, - this.height, - required this.initialDate, - required this.firstDate, - required this.lastDate, - this.selectableDayPredicate, - required this.initialDatePickerMode, - required this.era, - this.locale, - required this.borderRadius, - this.imageHeader, - this.description = "", - this.fontFamily, - this.textNegativeButton, - this.textPositiveButton, - this.textActionButton, - this.onTapActionButton, - this.styleDatePicker, - this.styleYearPicker, - this.customWeekDays, - this.builderDay, - this.listDateDisabled, - this.onTapDay, - this.onMonthChange}) - : super(key: key); - - final DateTime initialDate; - final DateTime firstDate; - final DateTime lastDate; - final SelectableDayPredicate? selectableDayPredicate; - final DatePickerMode initialDatePickerMode; - - /// double height. - final double? height; - - /// Custom era year. - final EraMode era; - final Locale? locale; - - /// Border - final double borderRadius; - - /// Header; - final ImageProvider? imageHeader; - final String description; - - /// Font - final String? fontFamily; - - /// Button - final String? textNegativeButton; - final String? textPositiveButton; - final String? textActionButton; - - final VoidCallback? onTapActionButton; - - /// Style - final MaterialRoundedDatePickerStyle? styleDatePicker; - final MaterialRoundedYearPickerStyle? styleYearPicker; - - /// Custom Weekday - final List? customWeekDays; - - final BuilderDayOfDatePicker? builderDay; - - final List? listDateDisabled; - final OnTapDay? onTapDay; - - final Function? onMonthChange; - - @override - _FlutterRoundedDatePickerDialogState createState() => - _FlutterRoundedDatePickerDialogState(); -} - -class _FlutterRoundedDatePickerDialogState - extends State { - @override - void initState() { - super.initState(); - _selectedDate = widget.initialDate; - _mode = widget.initialDatePickerMode; - } - - bool _announcedInitialDate = false; - - late MaterialLocalizations localizations; - late TextDirection textDirection; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - localizations = MaterialLocalizations.of(context); - textDirection = Directionality.of(context); - if (!_announcedInitialDate) { - _announcedInitialDate = true; - SemanticsService.announce( - localizations.formatFullDate(_selectedDate), - textDirection, - ); - } - } - - late DateTime _selectedDate; - late DatePickerMode _mode; - final GlobalKey _pickerKey = GlobalKey(); - - void _vibrate() { - switch (Theme.of(context).platform) { - case TargetPlatform.android: - case TargetPlatform.fuchsia: - HapticFeedback.vibrate(); - break; - case TargetPlatform.iOS: - default: - break; - } - } - - void _handleModeChanged(DatePickerMode mode) { - _vibrate(); - setState(() { - _mode = mode; - if (_mode == DatePickerMode.day) { - SemanticsService.announce( - localizations.formatMonthYear(_selectedDate), - textDirection, - ); - } else { - SemanticsService.announce( - localizations.formatYear(_selectedDate), - textDirection, - ); - } - }); - } - - Future _handleYearChanged(DateTime value) async { - if (value.isBefore(widget.firstDate)) { - value = widget.firstDate; - } else if (value.isAfter(widget.lastDate)) { - value = widget.lastDate; - } - if (value == _selectedDate) return; - - if (widget.onMonthChange != null) await widget.onMonthChange!(value); - - _vibrate(); - setState(() { - _mode = DatePickerMode.day; - _selectedDate = value; - }); - } - - void _handleDayChanged(DateTime value) { - _vibrate(); - setState(() { - _selectedDate = value; - }); - } - - void _handleCancel() { - Navigator.of(context).pop(); - } - - void _handleOk() { - Navigator.of(context).pop(_selectedDate); - } - - Widget _buildPicker() { - switch (_mode) { - case DatePickerMode.year: - return FlutterRoundedYearPicker( - key: _pickerKey, - selectedDate: _selectedDate, - onChanged: (DateTime date) async => await _handleYearChanged(date), - firstDate: widget.firstDate, - lastDate: widget.lastDate, - era: widget.era, - fontFamily: widget.fontFamily, - style: widget.styleYearPicker, - ); - case DatePickerMode.day: - default: - return FlutterRoundedMonthPicker( - key: _pickerKey, - selectedDate: _selectedDate, - onChanged: _handleDayChanged, - firstDate: widget.firstDate, - lastDate: widget.lastDate, - era: widget.era, - locale: widget.locale, - selectableDayPredicate: widget.selectableDayPredicate, - fontFamily: widget.fontFamily, - style: widget.styleDatePicker, - borderRadius: widget.borderRadius, - customWeekDays: widget.customWeekDays, - builderDay: widget.builderDay, - listDateDisabled: widget.listDateDisabled, - onTapDay: widget.onTapDay, - onMonthChange: widget.onMonthChange); - } - } - - @override - Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - final Widget picker = _buildPicker(); - final isDesktop = Util.isDesktop; - - final Widget actions = FlutterRoundedButtonAction( - textButtonNegative: widget.textNegativeButton, - textButtonPositive: widget.textPositiveButton, - onTapButtonNegative: _handleCancel, - onTapButtonPositive: _handleOk, - textActionButton: widget.textActionButton, - onTapButtonAction: widget.onTapActionButton, - localizations: localizations, - textStyleButtonNegative: widget.styleDatePicker?.textStyleButtonNegative, - textStyleButtonPositive: widget.styleDatePicker?.textStyleButtonPositive, - textStyleButtonAction: widget.styleDatePicker?.textStyleButtonAction, - borderRadius: widget.borderRadius, - paddingActionBar: widget.styleDatePicker?.paddingActionBar, - background: widget.styleDatePicker?.backgroundActionBar, - ); - - Color backgroundPicker = theme.dialogBackgroundColor; - if (_mode == DatePickerMode.day) { - backgroundPicker = widget.styleDatePicker?.backgroundPicker ?? - theme.dialogBackgroundColor; - } else { - backgroundPicker = widget.styleYearPicker?.backgroundPicker ?? - theme.dialogBackgroundColor; - } - - final Dialog dialog = Dialog( - child: OrientationBuilder( - builder: (BuildContext context, Orientation orientation) { - final Widget header = FlutterRoundedDatePickerHeader( - selectedDate: _selectedDate, - mode: _mode, - onModeChanged: _handleModeChanged, - orientation: orientation, - era: widget.era, - borderRadius: widget.borderRadius, - imageHeader: widget.imageHeader, - description: widget.description, - fontFamily: widget.fontFamily, - style: widget.styleDatePicker); - switch (orientation) { - case Orientation.landscape: - return Container( - height: isDesktop ? 600 : null, - width: isDesktop ? 700 : null, - decoration: BoxDecoration( - color: backgroundPicker, - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible(flex: 1, child: header), - Flexible( - flex: 2, // have the picker take up 2/3 of the dialog width - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox( - height: isDesktop ? 530 : null, - width: isDesktop ? 700 : null, - child: picker), - actions, - ], - ), - ), - ], - ), - ); - case Orientation.portrait: - default: - return Container( - decoration: BoxDecoration( - color: backgroundPicker, - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - header, - if (widget.height == null) - Flexible(child: picker) - else - SizedBox( - height: widget.height, - child: picker, - ), - actions, - ], - ), - ); - } - }), - ); - - return Theme( - data: theme.copyWith(dialogBackgroundColor: Colors.transparent), - child: dialog, - ); - } -} diff --git a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart b/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart deleted file mode 100644 index 6f6db0580..000000000 --- a/lib/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart +++ /dev/null @@ -1,226 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -// import 'package:flutter_rounded_date_picker/src/dialogs/flutter_rounded_date_picker_dialog.dart'; -import 'package:flutter_rounded_date_picker/src/era_mode.dart'; -import 'package:flutter_rounded_date_picker/src/material_rounded_date_picker_style.dart'; -import 'package:flutter_rounded_date_picker/src/material_rounded_year_picker_style.dart'; -import 'package:flutter_rounded_date_picker/src/widgets/flutter_rounded_day_picker.dart'; -import 'package:stackwallet/widgets/rounded_date_picker/flutter_rounded_date_picker_dialog.dart'; - -/// -/// This file uses code taken from https://github.com/benznest/flutter_rounded_date_picker -/// - -// Examples can assume: -// BuildContext context; - -/// Initial display mode of the date picker dialog. -/// -/// Date picker UI mode for either showing a list of available years or a -/// monthly calendar initially in the dialog shown by calling [showDatePicker]. -/// - -// Shows the selected date in large font and toggles between year and day mode - -/// Signature for predicating dates for enabled date selections. -/// -/// See [showDatePicker]. -typedef SelectableDayPredicate = bool Function(DateTime day); - -/// Shows a dialog containing a material design date picker. -/// -/// The returned [Future] resolves to the date selected by the user when the -/// user closes the dialog. If the user cancels the dialog, null is returned. -/// -/// An optional [selectableDayPredicate] function can be passed in to customize -/// the days to enable for selection. If provided, only the days that -/// [selectableDayPredicate] returned true for will be selectable. -/// -/// An optional [initialDatePickerMode] argument can be used to display the -/// date picker initially in the year or month+day picker mode. It defaults -/// to month+day, and must not be null. -/// -/// An optional [locale] argument can be used to set the locale for the date -/// picker. It defaults to the ambient locale provided by [Localizations]. -/// -/// An optional [textDirection] argument can be used to set the text direction -/// (RTL or LTR) for the date picker. It defaults to the ambient text direction -/// provided by [Directionality]. If both [locale] and [textDirection] are not -/// null, [textDirection] overrides the direction chosen for the [locale]. -/// -/// The [context] argument is passed to [showDialog], the documentation for -/// which discusses how it is used. -/// -/// The [builder] parameter can be used to wrap the dialog widget -/// to add inherited widgets like [Theme]. -/// -/// {@tool sample} -/// Show a date picker with the dark theme. -/// -/// ```dart -/// Future selectedDate = showDatePicker( -/// context: context, -/// initialDate: DateTime.now(), -/// firstDate: DateTime(2018), -/// lastDate: DateTime(2030), -/// builder: (BuildContext context, Widget child) { -/// return Theme( -/// data: ThemeData.dark(), -/// child: child, -/// ); -/// }, -/// ); -/// ``` -/// {@end-tool} -/// -/// The [context], [initialDate], [firstDate], and [lastDate] parameters must -/// not be null. -/// -/// See also: -/// -/// * [showTimePicker], which shows a dialog that contains a material design -/// time picker. -/// * [DayPicker], which displays the days of a given month and allows -/// choosing a day. -/// * [MonthPicker], which displays a scrollable list of months to allow -/// picking a month. -/// * [YearPicker], which displays a scrollable list of years to allow picking -/// a year. -/// - -Future showRoundedDatePicker( - {required BuildContext context, - double? height, - DateTime? initialDate, - DateTime? firstDate, - DateTime? lastDate, - SelectableDayPredicate? selectableDayPredicate, - DatePickerMode initialDatePickerMode = DatePickerMode.day, - Locale? locale, - TextDirection? textDirection, - ThemeData? theme, - double borderRadius = 16, - EraMode era = EraMode.CHRIST_YEAR, - ImageProvider? imageHeader, - String description = "", - String? fontFamily, - bool barrierDismissible = false, - Color background = Colors.transparent, - String? textNegativeButton, - String? textPositiveButton, - String? textActionButton, - VoidCallback? onTapActionButton, - MaterialRoundedDatePickerStyle? styleDatePicker, - MaterialRoundedYearPickerStyle? styleYearPicker, - List? customWeekDays, - BuilderDayOfDatePicker? builderDay, - List? listDateDisabled, - OnTapDay? onTapDay, - Function? onMonthChange}) async { - initialDate ??= DateTime.now(); - firstDate ??= DateTime(initialDate.year - 1); - lastDate ??= DateTime(initialDate.year + 1); - theme ??= ThemeData(); - - assert( - !initialDate.isBefore(firstDate), - 'initialDate must be on or after firstDate', - ); - assert( - !initialDate.isAfter(lastDate), - 'initialDate must be on or before lastDate', - ); - assert( - !firstDate.isAfter(lastDate), - 'lastDate must be on or after firstDate', - ); - assert( - selectableDayPredicate == null || selectableDayPredicate(initialDate), - 'Provided initialDate must satisfy provided selectableDayPredicate', - ); - assert( - (onTapActionButton != null && textActionButton != null) || - onTapActionButton == null, - "If you provide onLeftBtn, you must provide leftBtn", - ); - assert(debugCheckHasMaterialLocalizations(context)); - - Widget child = GestureDetector( - onTap: () { - if (!barrierDismissible) { - Navigator.pop(context); - } - }, - child: Container( - color: background, - child: GestureDetector( - onTap: () { - // - }, - child: FlutterRoundedDatePickerDialog( - height: height, - initialDate: initialDate, - firstDate: firstDate, - lastDate: lastDate, - selectableDayPredicate: selectableDayPredicate, - initialDatePickerMode: initialDatePickerMode, - era: era, - locale: locale, - borderRadius: borderRadius, - imageHeader: imageHeader, - description: description, - fontFamily: fontFamily, - textNegativeButton: textNegativeButton, - textPositiveButton: textPositiveButton, - textActionButton: textActionButton, - onTapActionButton: onTapActionButton, - styleDatePicker: styleDatePicker, - styleYearPicker: styleYearPicker, - customWeekDays: customWeekDays, - builderDay: builderDay, - listDateDisabled: listDateDisabled, - onTapDay: onTapDay, - onMonthChange: onMonthChange, - ), - ), - ), - ); - - if (textDirection != null) { - child = Directionality( - textDirection: textDirection, - child: child, - ); - } - - if (locale != null) { - child = Localizations.override( - context: context, - locale: locale, - child: child, - ); - } - - return await showDialog( - context: context, - barrierDismissible: barrierDismissible, - builder: (_) => Theme(data: theme!, child: child), - ); -} diff --git a/lib/widgets/stack_dialog.dart b/lib/widgets/stack_dialog.dart index be7f22ed9..bc247c2f2 100644 --- a/lib/widgets/stack_dialog.dart +++ b/lib/widgets/stack_dialog.dart @@ -147,8 +147,10 @@ class StackOkDialog extends StatelessWidget { this.icon, required this.title, this.message, + this.desktopPopRootNavigator = false, }) : super(key: key); + final bool desktopPopRootNavigator; final Widget? leftButton; final void Function(String)? onOkPressed; @@ -208,9 +210,13 @@ class StackOkDialog extends StatelessWidget { onOkPressed?.call("OK"); } : () { - int count = 0; - Navigator.of(context).popUntil((_) => count++ >= 2); - // onOkPressed?.call("OK"); + if (desktopPopRootNavigator) { + Navigator.of(context, rootNavigator: true).pop(); + } else { + int count = 0; + Navigator.of(context).popUntil((_) => count++ >= 2); + // onOkPressed?.call("OK"); + } }, style: Theme.of(context) .extension()! diff --git a/lib/widgets/textfields/frost_step_field.dart b/lib/widgets/textfields/frost_step_field.dart new file mode 100644 index 000000000..7fd19ba3b --- /dev/null +++ b/lib/widgets/textfields/frost_step_field.dart @@ -0,0 +1,182 @@ +import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class FrostStepField extends StatefulWidget { + const FrostStepField({ + super.key, + required this.controller, + required this.focusNode, + this.label, + this.hint, + required this.onChanged, + required this.showQrScanOption, + }); + + final TextEditingController controller; + final FocusNode focusNode; + final String? label; + final String? hint; + final void Function(String) onChanged; + final bool showQrScanOption; + + @override + State createState() => _FrostStepFieldState(); +} + +class _FrostStepFieldState extends State { + final _xKey = UniqueKey(); + final _pasteKey = UniqueKey(); + late final Key? _qrKey; + + bool _isEmpty = true; + + final _inputBorder = OutlineInputBorder( + borderSide: const BorderSide( + width: 0, + color: Colors.transparent, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ); + + late final void Function(String) _changed; + + @override + void initState() { + _qrKey = widget.showQrScanOption ? UniqueKey() : null; + _isEmpty = widget.controller.text.isEmpty; + + _changed = (value) { + if (context.mounted) { + widget.onChanged.call(value); + setState(() { + _isEmpty = widget.controller.text.isEmpty; + }); + } + }; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: widget.label != null, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + widget.label!, + style: STextStyles.w500_14(context), + ), + const SizedBox( + height: 4, + ), + child, + ], + ), + child: TextField( + controller: widget.controller, + focusNode: widget.focusNode, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + style: STextStyles.field(context), + onChanged: _changed, + decoration: InputDecoration( + hintText: widget.hint, + fillColor: widget.focusNode.hasFocus + ? Theme.of(context).extension()!.textFieldActiveBG + : Theme.of(context).extension()!.textFieldDefaultBG, + hintStyle: Util.isDesktop + ? STextStyles.desktopTextFieldLabel(context) + : STextStyles.fieldLabel(context), + enabledBorder: _inputBorder, + focusedBorder: _inputBorder, + errorBorder: _inputBorder, + disabledBorder: _inputBorder, + focusedErrorBorder: _inputBorder, + suffixIcon: Padding( + padding: _isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + !_isEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Frost Step Field Input.", + key: _xKey, + onTap: () { + widget.controller.text = ""; + + _changed(widget.controller.text); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Frost Step Field Input.", + key: _pasteKey, + onTap: () async { + final ClipboardData? data = + await Clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + widget.controller.text = data.text!.trim(); + } + + _changed(widget.controller.text); + }, + child: + _isEmpty ? const ClipboardIcon() : const XIcon(), + ), + if (_isEmpty && widget.showQrScanOption) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: _qrKey, + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await BarcodeScanner.scan(); + + widget.controller.text = qrResult.rawContent; + + _changed(widget.controller.text); + } on PlatformException catch (e, s) { + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index bc1c80aa4..9345cf271 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -105,7 +105,7 @@ class SimpleWalletCard extends ConsumerWidget { whileFuture: loadFuture, context: context, message: 'Opening ${wallet.info.name}', - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (popPrevious) nav.pop(); @@ -135,7 +135,7 @@ class SimpleWalletCard extends ConsumerWidget { context: desktopNavigatorState?.context ?? context, opaqueBG: true, message: "Loading ${contract.name}", - isDesktop: Util.isDesktop, + rootNavigator: Util.isDesktop, ); if (!success!) { diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart index aef410d47..55a897255 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart @@ -22,10 +22,10 @@ import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; class WalletInfoRowBalance extends ConsumerWidget { const WalletInfoRowBalance({ - Key? key, + super.key, required this.walletId, this.contractAddress, - }) : super(key: key); + }); final String walletId; final String? contractAddress; @@ -45,8 +45,11 @@ class WalletInfoRowBalance extends ConsumerWidget { } else { contract = MainDB.instance.getEthContractSync(contractAddress!)!; totalBalance = ref - .watch(pTokenBalance( - (contractAddress: contractAddress!, walletId: walletId))) + .watch( + pTokenBalance( + (walletId: walletId, contractAddress: contractAddress!), + ), + ) .total; } diff --git a/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart new file mode 100644 index 000000000..b91542a8e --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart @@ -0,0 +1,43 @@ +/* +* This file is part of Stack Wallet. +* +* Copyright (c) 2023 Cypher Stack +* All Rights Reserved. +* The code is distributed under GPLv3 license, see LICENSE file for details. +* Generated by Cypher Stack on 2023-05-26 +* +*/ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; + +class FrostSignNavIcon extends ConsumerWidget { + const FrostSignNavIcon({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .bottomNavIconIcon + .withOpacity(0.4), + borderRadius: BorderRadius.circular( + 24, + ), + ), + child: Padding( + padding: const EdgeInsets.all(6.0), + child: SvgPicture.asset( + Assets.svg.pencil, + width: 12, + height: 12, + color: Theme.of(context).extension()!.bottomNavIconIcon, + ), + ), + ); + } +} diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index bb9965d23..e1af526f4 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -17,6 +17,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST coinlib_flutter flutter_libsparkmobile + frostdart tor_ffi_plugin ) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index f76e35a57..35c7171cb 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -62,6 +62,8 @@ PODS: - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) + - frostdart (0.0.1): + - FlutterMacOS - isar_flutter_libs (1.0.0): - FlutterMacOS - lelantus (0.0.1): @@ -98,6 +100,7 @@ DEPENDENCIES: - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - frostdart (from `Flutter/ephemeral/.symlinks/plugins/frostdart/macos`) - isar_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos`) - lelantus (from `Flutter/ephemeral/.symlinks/plugins/lelantus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) @@ -140,6 +143,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: :path: Flutter/ephemeral + frostdart: + :path: Flutter/ephemeral/.symlinks/plugins/frostdart/macos isar_flutter_libs: :path: Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos lelantus: @@ -171,10 +176,11 @@ SPEC CHECKSUMS: device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_libepiccash: be1560a04150c5cc85bcf08d236ec2b3d1f5d8da - flutter_libsparkmobile: 8ae86b0ccc7e52c9db6b53e258ee2977deb184ab + flutter_libsparkmobile: df2d36af1691379c81249e7be7b68be3c81d388b flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + frostdart: e6bf3119527ccfbcec1b8767da6ede5bb4c4f716 isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a lelantus: 308e42c5a648598936a07a234471dd8cf8e687a0 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce @@ -189,4 +195,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index c2c2e62ad..eccafe6ba 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -31,6 +31,9 @@ B98151822A67402A009D013C /* mobileliblelantus.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = B98151802A674022009D013C /* mobileliblelantus.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B98151842A674143009D013C /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B98151832A674143009D013C /* libsqlite3.0.tbd */; }; BFD0376C00E1FFD46376BB9D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9206484E84CB0AD93E3E68CA /* Pods_RunnerTests.framework */; }; + F1FA2C4E2BA4B49F00BDA1BB /* frostdart.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = F1FA2C4D2BA4B49F00BDA1BB /* frostdart.dylib */; settings = {ATTRIBUTES = (Weak, ); }; }; + F1FA2C502BA4B4CA00BDA1BB /* frostdart.dylib in Resources */ = {isa = PBXBuildFile; fileRef = F1FA2C4F2BA4B4CA00BDA1BB /* frostdart.dylib */; }; + F1FA2C512BA4B51E00BDA1BB /* frostdart.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = F1FA2C4D2BA4B49F00BDA1BB /* frostdart.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; F653CA022D33E8B60E11A9F3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -58,6 +61,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + F1FA2C512BA4B51E00BDA1BB /* frostdart.dylib in Bundle Framework */, B98151822A67402A009D013C /* mobileliblelantus.framework in Bundle Framework */, ); name = "Bundle Framework"; @@ -94,6 +98,8 @@ B98151832A674143009D013C /* libsqlite3.0.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.0.tbd; path = usr/lib/libsqlite3.0.tbd; sourceTree = SDKROOT; }; BF5E76865ACB46314AC27D8F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F1FA2C4D2BA4B49F00BDA1BB /* frostdart.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = frostdart.dylib; path = ../crypto_plugins/frostdart/macos/frostdart.dylib; sourceTree = ""; }; + F1FA2C4F2BA4B4CA00BDA1BB /* frostdart.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = frostdart.dylib; path = ../crypto_plugins/frostdart/macos/frostdart.dylib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -111,6 +117,7 @@ files = ( B98151842A674143009D013C /* libsqlite3.0.tbd in Frameworks */, B98151812A674022009D013C /* mobileliblelantus.framework in Frameworks */, + F1FA2C4E2BA4B49F00BDA1BB /* frostdart.dylib in Frameworks */, F653CA022D33E8B60E11A9F3 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -140,6 +147,7 @@ 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( + F1FA2C4F2BA4B4CA00BDA1BB /* frostdart.dylib */, 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, @@ -196,6 +204,7 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + F1FA2C4D2BA4B49F00BDA1BB /* frostdart.dylib */, B98151832A674143009D013C /* libsqlite3.0.tbd */, B98151802A674022009D013C /* mobileliblelantus.framework */, E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */, @@ -268,7 +277,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { @@ -325,6 +334,7 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + F1FA2C502BA4B4CA00BDA1BB /* frostdart.dylib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -610,6 +620,17 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_monero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_shared_external/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_wownero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos/libs\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos\"", + /usr/lib/swift, + "$(PATH)/crypto_plugins/frostdart/macos\n", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -763,6 +784,17 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_monero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_shared_external/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_wownero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos/libs\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos\"", + /usr/lib/swift, + "$(PATH)/crypto_plugins/frostdart/macos\n", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -807,6 +839,17 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_monero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_shared_external/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/cw_wownero/macos/External/macos/lib\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos/libs\"", + "\"${PODS_ROOT}/../Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos\"", + /usr/lib/swift, + "$(PATH)/crypto_plugins/frostdart/macos\n", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a9d38bc3b..73f65e0e3 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.2.0-194.0.dev <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.4 <4.0.0" + flutter: ">=3.19.6" diff --git a/pubspec.yaml b/pubspec.yaml index b9b3a004e..a3fdcdf9d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,26 +11,29 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.10.2+214 +version: 2.0.0+222 environment: - sdk: ">=3.0.2 <4.0.0" - flutter: ^3.16.0 + sdk: ">=3.3.4 <4.0.0" + flutter: ^3.19.6 dependencies: flutter: sdk: flutter ffi: ^2.0.1 mutex: ^3.0.0 - websocket_universal: ^0.5.1 + web_socket_channel: ^2.4.5 lelantus: path: ./crypto_plugins/flutter_liblelantus + frostdart: + path: ./crypto_plugins/frostdart + flutter_libsparkmobile: git: url: https://github.com/cypherstack/flutter_libsparkmobile.git - ref: 3f986ca1a94bdac5d31373454c989cc2f5842de8 + ref: 439727b278250c61a291f5335c298c0f2d952517 flutter_libmonero: path: ./crypto_plugins/flutter_libmonero @@ -70,13 +73,13 @@ dependencies: fusiondart: git: url: https://github.com/cypherstack/fusiondart.git - ref: df8f7c627cfc77eaa3e364c0d166f3d04169ae05 + ref: 7dd8ff0dc9cb0caaac795fa44841a26437edfec3 # Utility plugins http: ^0.13.0 local_auth: ^1.1.10 permission_handler: ^11.0.0 - flutter_local_notifications: ^9.4.0 + flutter_local_notifications: ^17.0.0 rxdart: ^0.27.3 zxcvbn: ^1.0.0 dart_numerics: ^0.0.6 @@ -121,7 +124,6 @@ dependencies: decimal: ^2.1.0 event_bus: ^2.0.0 uuid: ^3.0.5 - flutter_rounded_date_picker: ^3.0.1 crypto: ^3.0.2 barcode_scan2: ^4.2.3 wakelock: ^0.6.2 @@ -138,7 +140,7 @@ dependencies: pointycastle: ^3.6.0 package_info_plus: ^4.0.2 lottie: ^2.3.2 - file_picker: ^5.5.0 + file_picker: ^8.0.3 connectivity_plus: ^4.0.1 isar: 3.0.5 isar_flutter_libs: 3.0.5 # contains the binaries @@ -153,31 +155,29 @@ dependencies: desktop_drop: ^0.4.1 nanodart: ^2.0.0 basic_utils: ^5.5.4 - stellar_flutter_sdk: ^1.5.3 - socks_socket: - git: - url: https://github.com/cypherstack/socks_socket.git - ref: master + stellar_flutter_sdk: ^1.7.8 bip340: ^0.2.0 # tezart: ^2.0.5 tezart: git: url: https://github.com/cypherstack/tezart.git - ref: 8a7070f533e63dd150edae99476f6853bfb25913 + ref: 13fa937ea9a9fc34caf047e068df9535f65c27ad socks5_proxy: ^1.0.3+dev.3 convert: ^3.1.1 flutter_hooks: ^0.20.3 meta: ^1.9.1 - coinlib_flutter: - git: - url: https://github.com/cypherstack/coinlib.git - path: coinlib_flutter - ref: 376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7 + coinlib_flutter: ^2.0.0 electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git - ref: 2897c6448e131241d4d91fe23fdab83305134225 + ref: 9e9441fc1e9ace8907256fff05fe2c607b0933b6 stream_channel: ^2.1.0 + solana: + git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream. + url: https://github.com/cypherstack/espresso-cash-public.git + ref: a83e375678eb22fe544dc125d29bbec0fb833882 + path: packages/solana + calendar_date_picker2: ^1.0.2 dev_dependencies: flutter_test: @@ -224,17 +224,6 @@ dependency_overrides: url: https://github.com/cypherstack/bip47.git ref: a6e7941b98a43a613708b1a12564bc17e712cfc7 - coinlib_flutter: - git: - url: https://github.com/cypherstack/coinlib.git - path: coinlib_flutter - ref: 376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7 - coinlib: - git: - url: https://github.com/cypherstack/coinlib.git - path: coinlib - ref: 376d520b4516d4eb7c3f0bd4b1522f7769f3f2a7 - # required for dart 3, at least until a fix is merged upstream wakelock_windows: git: @@ -263,6 +252,8 @@ dependency_overrides: crypto: 3.0.2 analyzer: ^5.2.0 pinenacl: ^0.3.3 + http: ^0.13.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index 3f499e3f9..5da88e0e3 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -10,13 +10,13 @@ mkdir -p build . ./config.sh ./install_ndk.sh -(cd ../../crypto_plugins/flutter_liblelantus/scripts/android && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libepiccash/scripts/android && ./install_ndk.sh && ./build_opensll.sh && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/android/ && ./build_all.sh ) & +PLUGINS_DIR=../../crypto_plugins + +(cd "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android && ./build_all.sh ) & +(cd "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android && ./build_all.sh ) & +(cd "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/ && ./build_all.sh ) && +set_rust_to_1720 && +(cd "${PLUGINS_DIR}"/frostdart/scripts/android && ./build_all.sh ) & wait echo "Done building" - -# set rust (back) to a more recent stable release to allow stack wallet to build tor -set_rust_to_1720 - diff --git a/scripts/android/install_ndk.sh b/scripts/android/install_ndk.sh index c36651516..0864a2bde 100755 --- a/scripts/android/install_ndk.sh +++ b/scripts/android/install_ndk.sh @@ -2,18 +2,20 @@ mkdir -p build . ./config.sh -TOOLCHAIN_DIR=${WORKDIR}/toolchain ANDROID_NDK_SHA256="8381c440fe61fcbb01e209211ac01b519cd6adf51ab1c2281d5daad6ca4c8c8c" if [ ! -e "$ANDROID_NDK_ZIP" ]; then - curl https://dl.google.com/android/repository/android-ndk-r20b-linux-x86_64.zip -o ${ANDROID_NDK_ZIP} + curl https://dl.google.com/android/repository/android-ndk-r20b-linux-x86_64.zip -o "${ANDROID_NDK_ZIP}" fi -echo $ANDROID_NDK_SHA256 $ANDROID_NDK_ZIP | sha256sum -c || exit 1 +echo "${ANDROID_NDK_SHA256}" "${ANDROID_NDK_ZIP}" | sha256sum -c || exit 1 -mkdir ../../crypto_plugins/flutter_libmonero/scripts/android/build -mkdir ../../crypto_plugins/flutter_liblelantus/scripts/android/build -mkdir ../../crypto_plugins/flutter_libepiccash/scripts/android/build -cp ${ANDROID_NDK_ZIP} ../../crypto_plugins/flutter_libmonero/scripts/android/build/ -cp ${ANDROID_NDK_ZIP} ../../crypto_plugins/flutter_liblelantus/scripts/android/build/ -cp ${ANDROID_NDK_ZIP} ../../crypto_plugins/flutter_libepiccash/scripts/android/build/ +PLUGINS_DIR=../../crypto_plugins + +mkdir -p "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/build +mkdir -p "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android/build +mkdir -p "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android/build + +cp "${ANDROID_NDK_ZIP}" "${PLUGINS_DIR}"/flutter_libmonero/scripts/android/build/ +cp "${ANDROID_NDK_ZIP}" "${PLUGINS_DIR}"/flutter_liblelantus/scripts/android/build/ +cp "${ANDROID_NDK_ZIP}" "${PLUGINS_DIR}"/flutter_libepiccash/scripts/android/build/ diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index dd6ad38ff..8b03d1b6f 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -16,14 +16,13 @@ rustup target add x86_64-apple-ios (cd ../../crypto_plugins/flutter_liblelantus/scripts/ios && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/ios && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh ) & +(cd ../../crypto_plugins/flutter_libmonero/scripts/ios/ && ./build_all.sh ) && +set_rust_to_1720 && +(cd ../../crypto_plugins/frostdart/scripts/ios && ./build_all.sh ) & wait echo "Done building" -# set rust (back) to a more recent stable release to allow stack wallet to build tor -set_rust_to_1720 - # ensure ios rust triples are there rustup target add aarch64-apple-ios rustup target add x86_64-apple-ios diff --git a/scripts/linux/build_all.sh b/scripts/linux/build_all.sh index 672668c13..d7c29e1b1 100755 --- a/scripts/linux/build_all.sh +++ b/scripts/linux/build_all.sh @@ -14,11 +14,11 @@ mkdir -p build ./build_secure_storage_deps.sh & (cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/linux && ./build_monero_all.sh && ./build_sharedfile.sh ) & +(cd ../../crypto_plugins/flutter_libmonero/scripts/linux && ./build_monero_all.sh && ./build_sharedfile.sh ) +set_rust_to_1720 +(cd ../../crypto_plugins/frostdart/scripts/linux && ./build_all.sh ) & + +./build_secp256k1.sh wait echo "Done building" - -# set rust (back) to a more recent stable release to allow stack wallet to build tor -set_rust_to_1720 - diff --git a/scripts/linux/build_secp256k1.sh b/scripts/linux/build_secp256k1.sh new file mode 100755 index 000000000..6fdd9f58c --- /dev/null +++ b/scripts/linux/build_secp256k1.sh @@ -0,0 +1,10 @@ +mkdir -p build +cd build +git clone https://github.com/bitcoin-core/secp256k1 +cd secp256k1 +mkdir -p build && cd build +cmake .. +cmake --build . +mkdir -p ../../../../../build +cp src/libsecp256k1.so.2.2.1 "../../../../../build/libsecp256k1.so" +cd ../../../ diff --git a/scripts/linux/build_secure_storage_deps.sh b/scripts/linux/build_secure_storage_deps.sh index e63e38665..aff3097dc 100755 --- a/scripts/linux/build_secure_storage_deps.sh +++ b/scripts/linux/build_secure_storage_deps.sh @@ -1,6 +1,7 @@ #!/bin/bash LINUX_DIRECTORY=$(pwd) JSONCPP_TAG=1.7.4 +LIBSECRET_TAG=0.21.4 mkdir -p build # Build JsonCPP @@ -24,8 +25,9 @@ cd "$LINUX_DIRECTORY" || exit 1 #pip3 install --user meson markdown tomli --upgrade # pip3 install --user gi-docgen cd build || exit 1 -git -C libsecret pull || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret +git -C libsecret pull origin $LIBSECRET_TAG || git clone https://gitlab.gnome.org/GNOME/libsecret.git libsecret cd libsecret || exit 1 +git checkout $LIBSECRET_TAG if ! [ -x "$(command -v meson)" ]; then echo 'Error: meson is not installed.' >&2 exit 1 diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh index 0e086fc71..59d1425c9 100755 --- a/scripts/macos/build_all.sh +++ b/scripts/macos/build_all.sh @@ -8,7 +8,9 @@ set_rust_to_1671 (cd ../../crypto_plugins/flutter_liblelantus/scripts/macos && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh ) & +(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh ) && +set_rust_to_1720 && +(cd ../../crypto_plugins/frostdart/scripts/macos && ./build_all.sh ) & wait echo "Done building" diff --git a/scripts/windows/build_all.sh b/scripts/windows/build_all.sh index ee3c1b558..c055cb6c3 100755 --- a/scripts/windows/build_all.sh +++ b/scripts/windows/build_all.sh @@ -9,10 +9,11 @@ set_rust_to_1671 mkdir -p build (cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_liblelantus/scripts/windows && ./build_all.sh ) & -(cd ../../crypto_plugins/flutter_libmonero/scripts/windows && ./build_all.sh) & +(cd ../../crypto_plugins/flutter_libmonero/scripts/windows && ./build_all.sh) +set_rust_to_1720 +(cd ../../crypto_plugins/frostdart/scripts/windows && ./build_all.sh ) & + +./build_secp256k1_wsl.sh wait echo "Done building" - -# set rust (back) to a more recent stable release to allow stack wallet to build tor -set_rust_to_1720 diff --git a/scripts/windows/build_secp256k1.bat b/scripts/windows/build_secp256k1.bat new file mode 100644 index 000000000..9e7433032 --- /dev/null +++ b/scripts/windows/build_secp256k1.bat @@ -0,0 +1,10 @@ +if not exist "build" mkdir "build" +cd build +rem git clone https://github.com/bitcoin-core/secp256k1 +cd secp256k1 +rem cmake -G "Visual Studio 17 2022" -A x64 -S . -B build +cd build +rem cmake --build . +if not exist "..\..\..\..\build\" mkdir "..\..\..\..\build\" +xcopy src\Debug\libsecp256k1-2.dll "..\..\..\..\build\secp256k1.dll" /Y +cd ..\..\..\ diff --git a/scripts/windows/build_secp256k1_wsl.sh b/scripts/windows/build_secp256k1_wsl.sh new file mode 100644 index 000000000..b5d2e281f --- /dev/null +++ b/scripts/windows/build_secp256k1_wsl.sh @@ -0,0 +1,10 @@ +mkdir -p build +cd build +git clone https://github.com/bitcoin-core/secp256k1 +cd secp256k1 +mkdir -p build && cd build +cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/x86_64-w64-mingw32.toolchain.cmake +cmake --build . +mkdir -p ../../../../../build +cp src/libsecp256k1-2.dll "../../../../../build/secp256k1.dll" +cd ../../../ diff --git a/scripts/windows/deps.ps1 b/scripts/windows/deps.ps1 deleted file mode 100644 index 2d90ed7fe..000000000 --- a/scripts/windows/deps.ps1 +++ /dev/null @@ -1,75 +0,0 @@ -# Create C:\development -New-Item -Path 'C:\development' -ItemType Directory -ErrorAction Ignore - -# $wc = [System.Net.WebClient]::new() -# $publishedHash = '8E28E54D601F0751922DE24632C1E716B4684876255CF82304A9B19E89A9CCAC' -# $FileHash = Get-FileHash -InputStream ($wc.OpenRead("C:\development\flutter_windows_3.7.12-stable.zip")) - -# if (-Not [System.IO.File]::Exists("C:\development\flutter_windows_3.7.12-stable.zip") or -Not ($FileHash.Hash -eq $publishedHash)) { -# } else { -# Download flutter_windows_3.7.12-stable.zip -# Write-Output "Downloading flutter_windows_3.7.12-stable.zip" -# $ProgressPreference = 'SilentlyContinue' # Speed up download process, see https://stackoverflow.com/questions/28682642/powershell-why-is-using-invoke-webrequest-much-slower-than-a-browser-download -# Invoke-WebRequest "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.7.12-stable.zip" -OutFile "C:\development\flutter_windows_3.7.12-stable.zip" -# } - -# Extract Flutter SDK -Write-Output "Extracting flutter_windows_3.7.12-stable.zip" -$progressPreference = 'SilentlyContinue' # Speed up extraction process, see https://github.com/PowerShell/Microsoft.PowerShell.Archive/issues/32#issuecomment-642582179 -# Add-MpPreference -ExclusionPath C:\development -# Expand-Archive "C:\development\flutter_windows_3.7.12-stable.zip" -DestinationPath "C:\development" -Add-Type -Assembly "System.IO.Compression.Filesystem" -[System.IO.Compression.ZipFile]::ExtractToDirectory("C:\development\flutter_windows_3.7.12-stable.zip", "C:\development") - -# See https://stackoverflow.com/a/69239861 -function Add-Path { - - param( - [Parameter(Mandatory, Position=0)] - [string] $LiteralPath, - [ValidateSet('User', 'CurrentUser', 'Machine', 'LocalMachine')] - [string] $Scope - ) - - Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop' - - $isMachineLevel = $Scope -in 'Machine', 'LocalMachine' - if ($isMachineLevel -and -not $($ErrorActionPreference = 'Continue'; net session 2>$null)) { throw "You must run AS ADMIN to update the machine-level Path environment variable." } - - $regPath = 'registry::' + ('HKEY_CURRENT_USER\Environment', 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment')[$isMachineLevel] - - # Note the use of the .GetValue() method to ensure that the *unexpanded* value is returned. - $currDirs = (Get-Item -LiteralPath $regPath).GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split ';' -ne '' - - if ($LiteralPath -in $currDirs) { - Write-Verbose "Already present in the persistent $(('user', 'machine')[$isMachineLevel])-level Path: $LiteralPath" - return - } - - $newValue = ($currDirs + $LiteralPath) -join ';' - - # Update the registry. - Set-ItemProperty -Type ExpandString -LiteralPath $regPath Path $newValue - - # Broadcast WM_SETTINGCHANGE to get the Windows shell to reload the - # updated environment, via a dummy [Environment]::SetEnvironmentVariable() operation. - $dummyName = [guid]::NewGuid().ToString() - [Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User') - [Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User') - - # Finally, also update the current session's `$env:Path` definition. - # Note: For simplicity, we always append to the in-process *composite* value, - # even though for a -Scope Machine update this isn't strictly the same. - $env:Path = ($env:Path -replace ';$') + ';' + $LiteralPath - - Write-Verbose "`"$LiteralPath`" successfully appended to the persistent $(('user', 'machine')[$isMachineLevel])-level Path and also the current-process value." - -} - -# Add Flutter SDK to PATH if it's not there already -if ($Env:Path -split ";" -contains 'C:\development\flutter\bin') { - Write-Output "Flutter SDK in PATH, done" -} else { - Write-Output "Attempting to add Flutter SDK to PATH" - Add-Path("C:\development\flutter\bin") -} diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 53ddd8cd6..cb1317096 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -3,19 +3,21 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:ui' as _i11; +import 'dart:async' as _i6; +import 'dart:ui' as _i12; -import 'package:decimal/decimal.dart' as _i2; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i9; -import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i8; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i10; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i7; -import 'package:stackwallet/utilities/prefs.dart' as _i6; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i5; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i10; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i9; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i11; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i8; +import 'package:stackwallet/utilities/prefs.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' - as _i3; + as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -28,8 +30,9 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_i // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -38,8 +41,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -48,8 +51,18 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeFusionInfo_2 extends _i1.SmartFake implements _i3.FusionInfo { - _FakeFusionInfo_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFusionInfo_3 extends _i1.SmartFake implements _i4.FusionInfo { + _FakeFusionInfo_3( Object parent, Invocation parentInvocation, ) : super( @@ -61,13 +74,21 @@ class _FakeFusionInfo_2 extends _i1.SmartFake implements _i3.FusionInfo { /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i5.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -91,7 +112,7 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -112,7 +133,16 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: false, ) as bool); @override - _i5.Future request({ + _i6.Future closeAdapter() => (super.noSuchMethod( + Invocation.method( + #closeAdapter, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override + _i6.Future request({ required String? command, List? args = const [], String? requestID, @@ -131,12 +161,12 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future>> batchRequest({ + _i6.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -151,11 +181,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #retries: retries, }, ), - returnValue: _i5.Future>>.value( - >[]), - ) as _i5.Future>>); + returnValue: _i6.Future>.value([]), + ) as _i6.Future>); @override - _i5.Future ping({ + _i6.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -168,10 +197,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i6.Future.value(false), + ) as _i6.Future); @override - _i5.Future> getBlockHeadTip({String? requestID}) => + _i6.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -179,10 +208,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future> getServerFeatures({String? requestID}) => + _i6.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -190,10 +219,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future broadcastTransaction({ + _i6.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -206,10 +235,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i5.Future.value(''), - ) as _i5.Future); + returnValue: _i6.Future.value(''), + ) as _i6.Future); @override - _i5.Future> getBalance({ + _i6.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -223,10 +252,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future>> getHistory({ + _i6.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -239,23 +268,23 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i5.Future>>.value( + returnValue: _i6.Future>>.value( >[]), - ) as _i5.Future>>); + ) as _i6.Future>>); @override - _i5.Future>>> getBatchHistory( - {required Map>? args}) => + _i6.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i5.Future>>>.value( - >>{}), - ) as _i5.Future>>>); + returnValue: _i6.Future>>>.value( + >>[]), + ) as _i6.Future>>>); @override - _i5.Future>> getUTXOs({ + _i6.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -268,23 +297,23 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i5.Future>>.value( + returnValue: _i6.Future>>.value( >[]), - ) as _i5.Future>>); + ) as _i6.Future>>); @override - _i5.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i6.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i5.Future>>>.value( - >>{}), - ) as _i5.Future>>>); + returnValue: _i6.Future>>>.value( + >>[]), + ) as _i6.Future>>>); @override - _i5.Future> getTransaction({ + _i6.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -300,10 +329,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future> getLelantusAnonymitySet({ + _i6.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -319,10 +348,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future getLelantusMintData({ + _i6.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -335,10 +364,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future> getLelantusUsedCoinSerials({ + _i6.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -352,20 +381,20 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future getLelantusLatestCoinId({String? requestID}) => + _i6.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i5.Future.value(0), - ) as _i5.Future); + returnValue: _i6.Future.value(0), + ) as _i6.Future); @override - _i5.Future> getSparkAnonymitySet({ + _i6.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -381,10 +410,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future> getSparkUsedCoinsTags({ + _i6.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -397,10 +426,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i5.Future>.value({}), - ) as _i5.Future>); + returnValue: _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future>> getSparkMintMetaData({ + _i6.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -413,21 +442,21 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i5.Future>>.value( + returnValue: _i6.Future>>.value( >[]), - ) as _i5.Future>>); + ) as _i6.Future>>); @override - _i5.Future getSparkLatestCoinId({String? requestID}) => + _i6.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i5.Future.value(0), - ) as _i5.Future); + returnValue: _i6.Future.value(0), + ) as _i6.Future); @override - _i5.Future> getFeeRate({String? requestID}) => + _i6.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -435,10 +464,10 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i5.Future>.value({}), - ) as _i5.Future>); + _i6.Future>.value({}), + ) as _i6.Future>); @override - _i5.Future<_i2.Decimal> estimateFee({ + _i6.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -451,7 +480,7 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i6.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -462,15 +491,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { }, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i6.Future<_i3.Decimal>); @override - _i5.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i6.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i5.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i6.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -478,13 +507,13 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i5.Future<_i2.Decimal>); + ) as _i6.Future<_i3.Decimal>); } /// A class which mocks [Prefs]. /// /// See the documentation for Mockito's code generation for more information. -class MockPrefs extends _i1.Mock implements _i6.Prefs { +class MockPrefs extends _i1.Mock implements _i7.Prefs { MockPrefs() { _i1.throwOnMissingStub(this); } @@ -540,12 +569,12 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - _i7.SyncingType get syncType => (super.noSuchMethod( + _i8.SyncingType get syncType => (super.noSuchMethod( Invocation.getter(#syncType), - returnValue: _i7.SyncingType.currentWalletOnly, - ) as _i7.SyncingType); + returnValue: _i8.SyncingType.currentWalletOnly, + ) as _i8.SyncingType); @override - set syncType(_i7.SyncingType? syncType) => super.noSuchMethod( + set syncType(_i8.SyncingType? syncType) => super.noSuchMethod( Invocation.setter( #syncType, syncType, @@ -704,12 +733,12 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - _i8.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( + _i9.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( Invocation.getter(#backupFrequencyType), - returnValue: _i8.BackupFrequencyType.everyTenMinutes, - ) as _i8.BackupFrequencyType); + returnValue: _i9.BackupFrequencyType.everyTenMinutes, + ) as _i9.BackupFrequencyType); @override - set backupFrequencyType(_i8.BackupFrequencyType? backupFrequencyType) => + set backupFrequencyType(_i9.BackupFrequencyType? backupFrequencyType) => super.noSuchMethod( Invocation.setter( #backupFrequencyType, @@ -860,61 +889,61 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValue: false, ) as bool); @override - _i5.Future init() => (super.noSuchMethod( + _i6.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( + _i6.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( Invocation.method( #incrementCurrentNotificationIndex, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future isExternalCallsSet() => (super.noSuchMethod( + _i6.Future isExternalCallsSet() => (super.noSuchMethod( Invocation.method( #isExternalCallsSet, [], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i6.Future.value(false), + ) as _i6.Future); @override - _i5.Future saveUserID(String? userId) => (super.noSuchMethod( + _i6.Future saveUserID(String? userId) => (super.noSuchMethod( Invocation.method( #saveUserID, [userId], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i5.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( + _i6.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( Invocation.method( #saveSignupEpoch, [signupEpoch], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i9.AmountUnit amountUnit(_i10.Coin? coin) => (super.noSuchMethod( + _i10.AmountUnit amountUnit(_i11.Coin? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i9.AmountUnit.normal, - ) as _i9.AmountUnit); + returnValue: _i10.AmountUnit.normal, + ) as _i10.AmountUnit); @override void updateAmountUnit({ - required _i10.Coin? coin, - required _i9.AmountUnit? amountUnit, + required _i11.Coin? coin, + required _i10.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -928,7 +957,7 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - int maxDecimals(_i10.Coin? coin) => (super.noSuchMethod( + int maxDecimals(_i11.Coin? coin) => (super.noSuchMethod( Invocation.method( #maxDecimals, [coin], @@ -937,7 +966,7 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { ) as int); @override void updateMaxDecimals({ - required _i10.Coin? coin, + required _i11.Coin? coin, required int? maxDecimals, }) => super.noSuchMethod( @@ -952,23 +981,23 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - _i3.FusionInfo getFusionServerInfo(_i10.Coin? coin) => (super.noSuchMethod( + _i4.FusionInfo getFusionServerInfo(_i11.Coin? coin) => (super.noSuchMethod( Invocation.method( #getFusionServerInfo, [coin], ), - returnValue: _FakeFusionInfo_2( + returnValue: _FakeFusionInfo_3( this, Invocation.method( #getFusionServerInfo, [coin], ), ), - ) as _i3.FusionInfo); + ) as _i4.FusionInfo); @override void setFusionServerInfo( - _i10.Coin? coin, - _i3.FusionInfo? fusionServerInfo, + _i11.Coin? coin, + _i4.FusionInfo? fusionServerInfo, ) => super.noSuchMethod( Invocation.method( @@ -981,7 +1010,7 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - void addListener(_i11.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -989,7 +1018,7 @@ class MockPrefs extends _i1.Mock implements _i6.Prefs { returnValueForMissingStub: null, ); @override - void removeListener(_i11.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], diff --git a/test/electrumx_test.dart b/test/electrumx_test.dart index 64dc69a58..06ed6111d 100644 --- a/test/electrumx_test.dart +++ b/test/electrumx_test.dart @@ -1,1778 +1,1778 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; -import 'package:stackwallet/electrumx_rpc/rpc.dart'; -import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; -import 'package:stackwallet/services/tor_service.dart'; -import 'package:stackwallet/utilities/prefs.dart'; - -import 'electrumx_test.mocks.dart'; -import 'sample_data/get_anonymity_set_sample_data.dart'; -import 'sample_data/get_used_serials_sample_data.dart'; -import 'sample_data/transaction_data_samples.dart'; - -@GenerateMocks([JsonRPC, Prefs, TorService]) -void main() { - group("factory constructors and getters", () { - test("electrumxnode .from factory", () { - final nodeA = ElectrumXNode( - address: "some address", - port: 1, - name: "some name", - id: "some ID", - useSSL: true, - ); - - final nodeB = ElectrumXNode.from(nodeA); - - expect(nodeB.toString(), nodeA.toString()); - expect(nodeA == nodeB, false); - }); - - test("electrumx .from factory", () { - final node = ElectrumXNode( - address: "some address", - port: 1, - name: "some name", - id: "some ID", - useSSL: true, - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - - final client = ElectrumXClient.from( - node: node, - failovers: [], - prefs: mockPrefs, - torService: torService, - ); - - expect(client.useSSL, node.useSSL); - expect(client.host, node.address); - expect(client.port, node.port); - expect(client.rpcClient, null); - - verifyNoMoreInteractions(mockPrefs); - }); - }); - - test("Server error", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "error": { - "code": 1, - "message": "None should be a transaction hash", - }, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - failovers: [], - prefs: mockPrefs, - torService: torService, - ); - - expect(() => client.getTransaction(requestID: "some requestId", txHash: ''), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - group("getBlockHeadTip", () { - test("getBlockHeadTip success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.headers.subscribe"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": {"height": 520481, "hex": "some block hex string"}, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = - await (client.getBlockHeadTip(requestID: "some requestId")); - - expect(result["height"], 520481); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getBlockHeadTip throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.headers.subscribe"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.getBlockHeadTip(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("ping", () { - test("ping success", () async { - final mockClient = MockJsonRPC(); - const command = "server.ping"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 2), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": null, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.ping(requestID: "some requestId"); - - expect(result, true); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("ping throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "server.ping"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 2), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.ping(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getServerFeatures", () { - test("getServerFeatures success", () async { - final mockClient = MockJsonRPC(); - const command = "server.features"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": { - "genesis_hash": - "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", - "hosts": { - "0.0.0.0": {"tcp_port": 51001, "ssl_port": 51002} - }, - "protocol_max": "1.0", - "protocol_min": "1.0", - "pruning": null, - "server_version": "ElectrumX 1.0.17", - "hash_function": "sha256" - }, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = - await client.getServerFeatures(requestID: "some requestId"); - - expect(result, { - "genesis_hash": - "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", - "hosts": { - "0.0.0.0": {"tcp_port": 51001, "ssl_port": 51002} - }, - "protocol_max": "1.0", - "protocol_min": "1.0", - "pruning": null, - "server_version": "ElectrumX 1.0.17", - "hash_function": "sha256", - }); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getServerFeatures throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "server.features"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.getServerFeatures(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("broadcastTransaction", () { - test("broadcastTransaction success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.broadcast"; - const jsonArgs = '["some raw transaction string"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": "the txid of the rawtx", - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.broadcastTransaction( - rawTx: "some raw transaction string", requestID: "some requestId"); - - expect(result, "the txid of the rawtx"); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("broadcastTransaction throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.broadcast"; - const jsonArgs = '["some raw transaction string"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.broadcastTransaction( - rawTx: "some raw transaction string", - requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getBalance", () { - test("getBalance success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.get_balance"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": { - "confirmed": 103873966, - "unconfirmed": 23684400, - }, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getBalance( - scripthash: "dummy hash", requestID: "some requestId"); - - expect(result, {"confirmed": 103873966, "unconfirmed": 23684400}); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getBalance throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.get_balance"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getBalance( - scripthash: "dummy hash", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getHistory", () { - test("getHistory success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.get_history"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 5), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": [ - { - "height": 200004, - "tx_hash": - "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" - }, - { - "height": 215008, - "tx_hash": - "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" - } - ], - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getHistory( - scripthash: "dummy hash", requestID: "some requestId"); - - expect(result, [ - { - "height": 200004, - "tx_hash": - "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" - }, - { - "height": 215008, - "tx_hash": - "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" - } - ]); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getHistory throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.get_history"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 5), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getHistory( - scripthash: "dummy hash", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getUTXOs", () { - test("getUTXOs success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.listunspent"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": [ - { - "tx_pos": 0, - "value": 45318048, - "tx_hash": - "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", - "height": 437146 - }, - { - "tx_pos": 0, - "value": 919195, - "tx_hash": - "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", - "height": 441696 - } - ], - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getUTXOs( - scripthash: "dummy hash", requestID: "some requestId"); - - expect(result, [ - { - "tx_pos": 0, - "value": 45318048, - "tx_hash": - "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", - "height": 437146 - }, - { - "tx_pos": 0, - "value": 919195, - "tx_hash": - "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", - "height": 441696 - } - ]); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getUTXOs throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.scripthash.listunspent"; - const jsonArgs = '["dummy hash"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getUTXOs( - scripthash: "dummy hash", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getTransaction", () { - test("getTransaction success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getTransaction throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getAnonymitySet", () { - test("getAnonymitySet success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getanonymityset"; - const jsonArgs = '["1",""]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": GetAnonymitySetSampleData.data, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusAnonymitySet( - groupId: "1", blockhash: "", requestID: "some requestId"); - - expect(result, GetAnonymitySetSampleData.data); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getAnonymitySet throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getanonymityset"; - const jsonArgs = '["1",""]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusAnonymitySet( - groupId: "1", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getMintData", () { - test("getMintData success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getmintmetadata"; - const jsonArgs = '["some mints"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": "mint meta data", - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusMintData( - mints: "some mints", requestID: "some requestId"); - - expect(result, "mint meta data"); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getMintData throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getmintmetadata"; - const jsonArgs = '["some mints"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusMintData( - mints: "some mints", requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getUsedCoinSerials", () { - test("getUsedCoinSerials success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getusedcoinserials"; - const jsonArgs = '["0"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 2), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": GetUsedSerialsSampleData.serials, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusUsedCoinSerials( - requestID: "some requestId", startNumber: 0); - - expect(result, GetUsedSerialsSampleData.serials); - - verify(mockPrefs.wifiOnly).called(3); - verify(mockPrefs.useTor).called(3); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getUsedCoinSerials throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getusedcoinserials"; - const jsonArgs = '["0"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 2), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusUsedCoinSerials( - requestID: "some requestId", startNumber: 0), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getLatestCoinId", () { - test("getLatestCoinId success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getlatestcoinid"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": 1, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = - await client.getLelantusLatestCoinId(requestID: "some requestId"); - - expect(result, 1); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getLatestCoinId throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getlatestcoinid"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusLatestCoinId( - requestID: "some requestId", - ), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getCoinsForRecovery", () { - test("getCoinsForRecovery success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getanonymityset"; - const jsonArgs = '["1",""]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": GetAnonymitySetSampleData.data, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusAnonymitySet( - groupId: "1", blockhash: "", requestID: "some requestId"); - - expect(result, GetAnonymitySetSampleData.data); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getAnonymitySet throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getanonymityset"; - const jsonArgs = '["1",""]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusAnonymitySet( - groupId: "1", - requestID: "some requestId", - ), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getMintData", () { - test("getMintData success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getmintmetadata"; - const jsonArgs = '["some mints"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": "mint meta data", - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusMintData( - mints: "some mints", requestID: "some requestId"); - - expect(result, "mint meta data"); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getMintData throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getmintmetadata"; - const jsonArgs = '["some mints"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusMintData( - mints: "some mints", - requestID: "some requestId", - ), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getUsedCoinSerials", () { - test("getUsedCoinSerials success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getusedcoinserials"; - const jsonArgs = '["0"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 2), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": GetUsedSerialsSampleData.serials, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getLelantusUsedCoinSerials( - requestID: "some requestId", startNumber: 0); - - expect(result, GetUsedSerialsSampleData.serials); - - verify(mockPrefs.wifiOnly).called(3); - verify(mockPrefs.useTor).called(3); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getUsedCoinSerials throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getusedcoinserials"; - const jsonArgs = '["0"]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(minutes: 2), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect( - () => client.getLelantusUsedCoinSerials( - requestID: "some requestId", startNumber: 0), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getLatestCoinId", () { - test("getLatestCoinId success", () async { - final mockClient = MockJsonRPC(); - const command = "lelantus.getlatestcoinid"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": 1, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = - await client.getLelantusLatestCoinId(requestID: "some requestId"); - - expect(result, 1); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getLatestCoinId throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "lelantus.getlatestcoinid"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.getLelantusLatestCoinId(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - group("getFeeRate", () { - test("getFeeRate success", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.getfeerate"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": { - "rate": 1000, - }, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - final result = await client.getFeeRate(requestID: "some requestId"); - - expect(result, {"rate": 1000}); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - test("getFeeRate throws/fails", () { - final mockClient = MockJsonRPC(); - const command = "blockchain.getfeerate"; - const jsonArgs = '[]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenThrow(Exception()); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: torService, - failovers: []); - - expect(() => client.getFeeRate(requestID: "some requestId"), - throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - }); - - test("rpcClient is null throws with bad server info", () { - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((realInvocation) => false); - final torService = MockTorService(); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final client = ElectrumXClient( - client: null, - port: -10, - host: "_ :sa %", - useSSL: false, - prefs: mockPrefs, - torService: torService, - failovers: [], - ); - - expect(() => client.getFeeRate(), throwsA(isA())); - - verify(mockPrefs.wifiOnly).called(1); - verifyNoMoreInteractions(mockPrefs); - }); - - group("Tor tests", () { - // useTor is false, so no TorService calls should be made. - test("Tor not in use", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when(mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - )).thenAnswer((_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId", - })); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => false); - when(mockPrefs.torKillSwitch) - .thenAnswer((_) => false); // Or true, shouldn't matter. - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.disconnected); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - failovers: [], - prefs: mockPrefs, - torService: mockTorService, - ); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNever(mockPrefs.torKillSwitch); - verifyNoMoreInteractions(mockPrefs); - verifyNever(mockTorService.status); - verifyNoMoreInteractions(mockTorService); - }); - - // useTor is true, but TorService is not enabled and the killswitch is off, so a clearnet call should be made. - test("Tor in use but Tor unavailable and killswitch off", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when(mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - )).thenAnswer((_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId", - })); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => true); - when(mockPrefs.torKillSwitch).thenAnswer((_) => false); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.disconnected); - when(mockTorService.getProxyInfo()).thenAnswer((_) => ( - host: InternetAddress('1.2.3.4'), - port: -1 - )); // Port is set to -1 until Tor is enabled. - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: mockTorService, - failovers: []); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verify(mockPrefs.torKillSwitch).called(1); - verifyNoMoreInteractions(mockPrefs); - verify(mockTorService.status).called(1); - verifyNever(mockTorService.getProxyInfo()); - verifyNoMoreInteractions(mockTorService); - }); - - // useTor is true and TorService is enabled, so a TorService call should be made. - test("Tor in use and available", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when(mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - )).thenAnswer((_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId", - })); - when(mockClient.proxyInfo) - .thenAnswer((_) => (host: InternetAddress('1.2.3.4'), port: 42)); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => true); - when(mockPrefs.torKillSwitch).thenAnswer((_) => false); // Or true. - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.connected); - when(mockTorService.getProxyInfo()) - .thenAnswer((_) => (host: InternetAddress('1.2.3.4'), port: 42)); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - prefs: mockPrefs, - torService: mockTorService, - failovers: []); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockClient.proxyInfo).called(1); - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verifyNever(mockPrefs.torKillSwitch); - verifyNoMoreInteractions(mockPrefs); - verify(mockTorService.status).called(1); - verify(mockTorService.getProxyInfo()).called(1); - verifyNoMoreInteractions(mockTorService); - }); - - // useTor is true, but TorService is not enabled and the killswitch is on, so no TorService calls should be made. - test("killswitch enabled", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "error": { - "code": 1, - "message": "None should be a transaction hash", - }, - "id": "some requestId", - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => true); - when(mockPrefs.torKillSwitch).thenAnswer((_) => true); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.disconnected); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - failovers: [], - prefs: mockPrefs, - torService: mockTorService, - ); - - try { - var result = await client.getTransaction( - requestID: "some requestId", txHash: ''); - } catch (e) { - expect(e, isA()); - expect( - e.toString(), - equals( - "Exception: Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX")); - } - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verify(mockPrefs.torKillSwitch).called(1); - verifyNoMoreInteractions(mockPrefs); - verify(mockTorService.status).called(1); - verifyNoMoreInteractions(mockTorService); - }); - - // useTor is true but Tor is not enabled, but because the killswitch is off, a clearnet call should be made. - test("killswitch disabled", () async { - final mockClient = MockJsonRPC(); - const command = "blockchain.transaction.get"; - const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; - when( - mockClient.request( - '{"jsonrpc": "2.0", "id": "some requestId",' - '"method": "$command","params": $jsonArgs}', - const Duration(seconds: 60), - ), - ).thenAnswer( - (_) async => JsonRPCResponse(data: { - "jsonrpc": "2.0", - "result": SampleGetTransactionData.txData0, - "id": "some requestId" - }), - ); - - final mockPrefs = MockPrefs(); - when(mockPrefs.useTor).thenAnswer((_) => true); - when(mockPrefs.torKillSwitch).thenAnswer((_) => false); - when(mockPrefs.wifiOnly).thenAnswer((_) => false); - final mockTorService = MockTorService(); - when(mockTorService.status) - .thenAnswer((_) => TorConnectionStatus.disconnected); - - final client = ElectrumXClient( - host: "some server", - port: 0, - useSSL: true, - client: mockClient, - failovers: [], - prefs: mockPrefs, - torService: mockTorService, - ); - - final result = await client.getTransaction( - txHash: SampleGetTransactionData.txHash0, - verbose: true, - requestID: "some requestId"); - - expect(result, SampleGetTransactionData.txData0); - - verify(mockPrefs.wifiOnly).called(1); - verify(mockPrefs.useTor).called(1); - verify(mockPrefs.torKillSwitch).called(1); - verifyNoMoreInteractions(mockPrefs); - verify(mockTorService.status).called(1); - verifyNoMoreInteractions(mockTorService); - }); - }); -} +// import 'dart:io'; +// +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:mockito/annotations.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +// import 'package:stackwallet/electrumx_rpc/rpc.dart'; +// import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +// import 'package:stackwallet/services/tor_service.dart'; +// import 'package:stackwallet/utilities/prefs.dart'; +// +// import 'electrumx_test.mocks.dart'; +// import 'sample_data/get_anonymity_set_sample_data.dart'; +// import 'sample_data/get_used_serials_sample_data.dart'; +// import 'sample_data/transaction_data_samples.dart'; +// +// @GenerateMocks([JsonRPC, Prefs, TorService]) +// void main() { +// group("factory constructors and getters", () { +// test("electrumxnode .from factory", () { +// final nodeA = ElectrumXNode( +// address: "some address", +// port: 1, +// name: "some name", +// id: "some ID", +// useSSL: true, +// ); +// +// final nodeB = ElectrumXNode.from(nodeA); +// +// expect(nodeB.toString(), nodeA.toString()); +// expect(nodeA == nodeB, false); +// }); +// +// test("electrumx .from factory", () { +// final node = ElectrumXNode( +// address: "some address", +// port: 1, +// name: "some name", +// id: "some ID", +// useSSL: true, +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// +// final client = ElectrumXClient.from( +// node: node, +// failovers: [], +// prefs: mockPrefs, +// torService: torService, +// ); +// +// expect(client.useSSL, node.useSSL); +// expect(client.host, node.address); +// expect(client.port, node.port); +// expect(client.rpcClient, null); +// +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// test("Server error", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "error": { +// "code": 1, +// "message": "None should be a transaction hash", +// }, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// failovers: [], +// prefs: mockPrefs, +// torService: torService, +// ); +// +// expect(() => client.getTransaction(requestID: "some requestId", txHash: ''), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// group("getBlockHeadTip", () { +// test("getBlockHeadTip success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.headers.subscribe"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": {"height": 520481, "hex": "some block hex string"}, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = +// await (client.getBlockHeadTip(requestID: "some requestId")); +// +// expect(result["height"], 520481); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getBlockHeadTip throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.headers.subscribe"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.getBlockHeadTip(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("ping", () { +// test("ping success", () async { +// final mockClient = MockJsonRPC(); +// const command = "server.ping"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 2), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": null, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.ping(requestID: "some requestId"); +// +// expect(result, true); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("ping throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "server.ping"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 2), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.ping(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getServerFeatures", () { +// test("getServerFeatures success", () async { +// final mockClient = MockJsonRPC(); +// const command = "server.features"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": { +// "genesis_hash": +// "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", +// "hosts": { +// "0.0.0.0": {"tcp_port": 51001, "ssl_port": 51002} +// }, +// "protocol_max": "1.0", +// "protocol_min": "1.0", +// "pruning": null, +// "server_version": "ElectrumX 1.0.17", +// "hash_function": "sha256" +// }, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = +// await client.getServerFeatures(requestID: "some requestId"); +// +// expect(result, { +// "genesis_hash": +// "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", +// "hosts": { +// "0.0.0.0": {"tcp_port": 51001, "ssl_port": 51002} +// }, +// "protocol_max": "1.0", +// "protocol_min": "1.0", +// "pruning": null, +// "server_version": "ElectrumX 1.0.17", +// "hash_function": "sha256", +// }); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getServerFeatures throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "server.features"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.getServerFeatures(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("broadcastTransaction", () { +// test("broadcastTransaction success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.broadcast"; +// const jsonArgs = '["some raw transaction string"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": "the txid of the rawtx", +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.broadcastTransaction( +// rawTx: "some raw transaction string", requestID: "some requestId"); +// +// expect(result, "the txid of the rawtx"); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("broadcastTransaction throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.broadcast"; +// const jsonArgs = '["some raw transaction string"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.broadcastTransaction( +// rawTx: "some raw transaction string", +// requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getBalance", () { +// test("getBalance success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.get_balance"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": { +// "confirmed": 103873966, +// "unconfirmed": 23684400, +// }, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getBalance( +// scripthash: "dummy hash", requestID: "some requestId"); +// +// expect(result, {"confirmed": 103873966, "unconfirmed": 23684400}); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getBalance throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.get_balance"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getBalance( +// scripthash: "dummy hash", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getHistory", () { +// test("getHistory success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.get_history"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 5), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": [ +// { +// "height": 200004, +// "tx_hash": +// "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" +// }, +// { +// "height": 215008, +// "tx_hash": +// "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" +// } +// ], +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getHistory( +// scripthash: "dummy hash", requestID: "some requestId"); +// +// expect(result, [ +// { +// "height": 200004, +// "tx_hash": +// "acc3758bd2a26f869fcc67d48ff30b96464d476bca82c1cd6656e7d506816412" +// }, +// { +// "height": 215008, +// "tx_hash": +// "f3e1bf48975b8d6060a9de8884296abb80be618dc00ae3cb2f6cee3085e09403" +// } +// ]); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getHistory throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.get_history"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 5), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getHistory( +// scripthash: "dummy hash", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getUTXOs", () { +// test("getUTXOs success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.listunspent"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": [ +// { +// "tx_pos": 0, +// "value": 45318048, +// "tx_hash": +// "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", +// "height": 437146 +// }, +// { +// "tx_pos": 0, +// "value": 919195, +// "tx_hash": +// "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", +// "height": 441696 +// } +// ], +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getUTXOs( +// scripthash: "dummy hash", requestID: "some requestId"); +// +// expect(result, [ +// { +// "tx_pos": 0, +// "value": 45318048, +// "tx_hash": +// "9f2c45a12db0144909b5db269415f7319179105982ac70ed80d76ea79d923ebf", +// "height": 437146 +// }, +// { +// "tx_pos": 0, +// "value": 919195, +// "tx_hash": +// "3d2290c93436a3e964cfc2f0950174d8847b1fbe3946432c4784e168da0f019f", +// "height": 441696 +// } +// ]); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getUTXOs throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.scripthash.listunspent"; +// const jsonArgs = '["dummy hash"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getUTXOs( +// scripthash: "dummy hash", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getTransaction", () { +// test("getTransaction success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getTransaction throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getAnonymitySet", () { +// test("getAnonymitySet success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getanonymityset"; +// const jsonArgs = '["1",""]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": GetAnonymitySetSampleData.data, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusAnonymitySet( +// groupId: "1", blockhash: "", requestID: "some requestId"); +// +// expect(result, GetAnonymitySetSampleData.data); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getAnonymitySet throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getanonymityset"; +// const jsonArgs = '["1",""]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusAnonymitySet( +// groupId: "1", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getMintData", () { +// test("getMintData success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getmintmetadata"; +// const jsonArgs = '["some mints"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": "mint meta data", +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusMintData( +// mints: "some mints", requestID: "some requestId"); +// +// expect(result, "mint meta data"); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getMintData throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getmintmetadata"; +// const jsonArgs = '["some mints"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusMintData( +// mints: "some mints", requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getUsedCoinSerials", () { +// test("getUsedCoinSerials success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getusedcoinserials"; +// const jsonArgs = '["0"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 2), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": GetUsedSerialsSampleData.serials, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusUsedCoinSerials( +// requestID: "some requestId", startNumber: 0); +// +// expect(result, GetUsedSerialsSampleData.serials); +// +// verify(mockPrefs.wifiOnly).called(3); +// verify(mockPrefs.useTor).called(3); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getUsedCoinSerials throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getusedcoinserials"; +// const jsonArgs = '["0"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 2), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusUsedCoinSerials( +// requestID: "some requestId", startNumber: 0), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getLatestCoinId", () { +// test("getLatestCoinId success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getlatestcoinid"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": 1, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = +// await client.getLelantusLatestCoinId(requestID: "some requestId"); +// +// expect(result, 1); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getLatestCoinId throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getlatestcoinid"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusLatestCoinId( +// requestID: "some requestId", +// ), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getCoinsForRecovery", () { +// test("getCoinsForRecovery success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getanonymityset"; +// const jsonArgs = '["1",""]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": GetAnonymitySetSampleData.data, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusAnonymitySet( +// groupId: "1", blockhash: "", requestID: "some requestId"); +// +// expect(result, GetAnonymitySetSampleData.data); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getAnonymitySet throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getanonymityset"; +// const jsonArgs = '["1",""]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusAnonymitySet( +// groupId: "1", +// requestID: "some requestId", +// ), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getMintData", () { +// test("getMintData success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getmintmetadata"; +// const jsonArgs = '["some mints"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": "mint meta data", +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusMintData( +// mints: "some mints", requestID: "some requestId"); +// +// expect(result, "mint meta data"); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getMintData throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getmintmetadata"; +// const jsonArgs = '["some mints"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusMintData( +// mints: "some mints", +// requestID: "some requestId", +// ), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getUsedCoinSerials", () { +// test("getUsedCoinSerials success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getusedcoinserials"; +// const jsonArgs = '["0"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 2), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": GetUsedSerialsSampleData.serials, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getLelantusUsedCoinSerials( +// requestID: "some requestId", startNumber: 0); +// +// expect(result, GetUsedSerialsSampleData.serials); +// +// verify(mockPrefs.wifiOnly).called(3); +// verify(mockPrefs.useTor).called(3); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getUsedCoinSerials throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getusedcoinserials"; +// const jsonArgs = '["0"]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(minutes: 2), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect( +// () => client.getLelantusUsedCoinSerials( +// requestID: "some requestId", startNumber: 0), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getLatestCoinId", () { +// test("getLatestCoinId success", () async { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getlatestcoinid"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": 1, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = +// await client.getLelantusLatestCoinId(requestID: "some requestId"); +// +// expect(result, 1); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getLatestCoinId throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "lelantus.getlatestcoinid"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.getLelantusLatestCoinId(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// group("getFeeRate", () { +// test("getFeeRate success", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.getfeerate"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": { +// "rate": 1000, +// }, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// final result = await client.getFeeRate(requestID: "some requestId"); +// +// expect(result, {"rate": 1000}); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// test("getFeeRate throws/fails", () { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.getfeerate"; +// const jsonArgs = '[]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenThrow(Exception()); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: torService, +// failovers: []); +// +// expect(() => client.getFeeRate(requestID: "some requestId"), +// throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// }); +// +// test("rpcClient is null throws with bad server info", () { +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((realInvocation) => false); +// final torService = MockTorService(); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final client = ElectrumXClient( +// client: null, +// port: -10, +// host: "_ :sa %", +// useSSL: false, +// prefs: mockPrefs, +// torService: torService, +// failovers: [], +// ); +// +// expect(() => client.getFeeRate(), throwsA(isA())); +// +// verify(mockPrefs.wifiOnly).called(1); +// verifyNoMoreInteractions(mockPrefs); +// }); +// +// group("Tor tests", () { +// // useTor is false, so no TorService calls should be made. +// test("Tor not in use", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when(mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// )).thenAnswer((_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId", +// })); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => false); +// when(mockPrefs.torKillSwitch) +// .thenAnswer((_) => false); // Or true, shouldn't matter. +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.disconnected); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// failovers: [], +// prefs: mockPrefs, +// torService: mockTorService, +// ); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNever(mockPrefs.torKillSwitch); +// verifyNoMoreInteractions(mockPrefs); +// verifyNever(mockTorService.status); +// verifyNoMoreInteractions(mockTorService); +// }); +// +// // useTor is true, but TorService is not enabled and the killswitch is off, so a clearnet call should be made. +// test("Tor in use but Tor unavailable and killswitch off", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when(mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// )).thenAnswer((_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId", +// })); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => true); +// when(mockPrefs.torKillSwitch).thenAnswer((_) => false); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.disconnected); +// when(mockTorService.getProxyInfo()).thenAnswer((_) => ( +// host: InternetAddress('1.2.3.4'), +// port: -1 +// )); // Port is set to -1 until Tor is enabled. +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: mockTorService, +// failovers: []); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verify(mockPrefs.torKillSwitch).called(1); +// verifyNoMoreInteractions(mockPrefs); +// verify(mockTorService.status).called(1); +// verifyNever(mockTorService.getProxyInfo()); +// verifyNoMoreInteractions(mockTorService); +// }); +// +// // useTor is true and TorService is enabled, so a TorService call should be made. +// test("Tor in use and available", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when(mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId","method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// )).thenAnswer((_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId", +// })); +// when(mockClient.proxyInfo) +// .thenAnswer((_) => (host: InternetAddress('1.2.3.4'), port: 42)); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => true); +// when(mockPrefs.torKillSwitch).thenAnswer((_) => false); // Or true. +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.connected); +// when(mockTorService.getProxyInfo()) +// .thenAnswer((_) => (host: InternetAddress('1.2.3.4'), port: 42)); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// prefs: mockPrefs, +// torService: mockTorService, +// failovers: []); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockClient.proxyInfo).called(1); +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verifyNever(mockPrefs.torKillSwitch); +// verifyNoMoreInteractions(mockPrefs); +// verify(mockTorService.status).called(1); +// verify(mockTorService.getProxyInfo()).called(1); +// verifyNoMoreInteractions(mockTorService); +// }); +// +// // useTor is true, but TorService is not enabled and the killswitch is on, so no TorService calls should be made. +// test("killswitch enabled", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "error": { +// "code": 1, +// "message": "None should be a transaction hash", +// }, +// "id": "some requestId", +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => true); +// when(mockPrefs.torKillSwitch).thenAnswer((_) => true); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.disconnected); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// failovers: [], +// prefs: mockPrefs, +// torService: mockTorService, +// ); +// +// try { +// var result = await client.getTransaction( +// requestID: "some requestId", txHash: ''); +// } catch (e) { +// expect(e, isA()); +// expect( +// e.toString(), +// equals( +// "Exception: Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX")); +// } +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verify(mockPrefs.torKillSwitch).called(1); +// verifyNoMoreInteractions(mockPrefs); +// verify(mockTorService.status).called(1); +// verifyNoMoreInteractions(mockTorService); +// }); +// +// // useTor is true but Tor is not enabled, but because the killswitch is off, a clearnet call should be made. +// test("killswitch disabled", () async { +// final mockClient = MockJsonRPC(); +// const command = "blockchain.transaction.get"; +// const jsonArgs = '["${SampleGetTransactionData.txHash0}",true]'; +// when( +// mockClient.request( +// '{"jsonrpc": "2.0", "id": "some requestId",' +// '"method": "$command","params": $jsonArgs}', +// const Duration(seconds: 60), +// ), +// ).thenAnswer( +// (_) async => JsonRPCResponse(data: { +// "jsonrpc": "2.0", +// "result": SampleGetTransactionData.txData0, +// "id": "some requestId" +// }), +// ); +// +// final mockPrefs = MockPrefs(); +// when(mockPrefs.useTor).thenAnswer((_) => true); +// when(mockPrefs.torKillSwitch).thenAnswer((_) => false); +// when(mockPrefs.wifiOnly).thenAnswer((_) => false); +// final mockTorService = MockTorService(); +// when(mockTorService.status) +// .thenAnswer((_) => TorConnectionStatus.disconnected); +// +// final client = ElectrumXClient( +// host: "some server", +// port: 0, +// useSSL: true, +// client: mockClient, +// failovers: [], +// prefs: mockPrefs, +// torService: mockTorService, +// ); +// +// final result = await client.getTransaction( +// txHash: SampleGetTransactionData.txHash0, +// verbose: true, +// requestID: "some requestId"); +// +// expect(result, SampleGetTransactionData.txData0); +// +// verify(mockPrefs.wifiOnly).called(1); +// verify(mockPrefs.useTor).called(1); +// verify(mockPrefs.torKillSwitch).called(1); +// verifyNoMoreInteractions(mockPrefs); +// verify(mockTorService.status).called(1); +// verifyNoMoreInteractions(mockTorService); +// }); +// }); +// } diff --git a/test/electrumx_test.mocks.dart b/test/electrumx_test.mocks.dart deleted file mode 100644 index aaf3e0810..000000000 --- a/test/electrumx_test.mocks.dart +++ /dev/null @@ -1,758 +0,0 @@ -// Mocks generated by Mockito 5.4.2 from annotations -// in stackwallet/test/electrumx_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:io' as _i4; -import 'dart:ui' as _i11; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/rpc.dart' as _i2; -import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart' - as _i13; -import 'package:stackwallet/services/tor_service.dart' as _i12; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i9; -import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i8; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i10; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i7; -import 'package:stackwallet/utilities/prefs.dart' as _i6; -import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' - as _i3; -import 'package:tor_ffi_plugin/tor_ffi_plugin.dart' as _i14; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeJsonRPCResponse_1 extends _i1.SmartFake - implements _i2.JsonRPCResponse { - _FakeJsonRPCResponse_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeFusionInfo_2 extends _i1.SmartFake implements _i3.FusionInfo { - _FakeFusionInfo_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeInternetAddress_3 extends _i1.SmartFake - implements _i4.InternetAddress { - _FakeInternetAddress_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [JsonRPC]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockJsonRPC extends _i1.Mock implements _i2.JsonRPC { - MockJsonRPC() { - _i1.throwOnMissingStub(this); - } - - @override - bool get useSSL => (super.noSuchMethod( - Invocation.getter(#useSSL), - returnValue: false, - ) as bool); - @override - String get host => (super.noSuchMethod( - Invocation.getter(#host), - returnValue: '', - ) as String); - @override - int get port => (super.noSuchMethod( - Invocation.getter(#port), - returnValue: 0, - ) as int); - @override - Duration get connectionTimeout => (super.noSuchMethod( - Invocation.getter(#connectionTimeout), - returnValue: _FakeDuration_0( - this, - Invocation.getter(#connectionTimeout), - ), - ) as Duration); - @override - set proxyInfo(({_i4.InternetAddress host, int port})? _proxyInfo) => - super.noSuchMethod( - Invocation.setter( - #proxyInfo, - _proxyInfo, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future<_i2.JsonRPCResponse> request( - String? jsonRpcRequest, - Duration? requestTimeout, - ) => - (super.noSuchMethod( - Invocation.method( - #request, - [ - jsonRpcRequest, - requestTimeout, - ], - ), - returnValue: - _i5.Future<_i2.JsonRPCResponse>.value(_FakeJsonRPCResponse_1( - this, - Invocation.method( - #request, - [ - jsonRpcRequest, - requestTimeout, - ], - ), - )), - ) as _i5.Future<_i2.JsonRPCResponse>); - @override - _i5.Future disconnect({ - required String? reason, - bool? ignoreMutex = false, - }) => - (super.noSuchMethod( - Invocation.method( - #disconnect, - [], - { - #reason: reason, - #ignoreMutex: ignoreMutex, - }, - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); -} - -/// A class which mocks [Prefs]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockPrefs extends _i1.Mock implements _i6.Prefs { - MockPrefs() { - _i1.throwOnMissingStub(this); - } - - @override - bool get isInitialized => (super.noSuchMethod( - Invocation.getter(#isInitialized), - returnValue: false, - ) as bool); - @override - int get lastUnlockedTimeout => (super.noSuchMethod( - Invocation.getter(#lastUnlockedTimeout), - returnValue: 0, - ) as int); - @override - set lastUnlockedTimeout(int? lastUnlockedTimeout) => super.noSuchMethod( - Invocation.setter( - #lastUnlockedTimeout, - lastUnlockedTimeout, - ), - returnValueForMissingStub: null, - ); - @override - int get lastUnlocked => (super.noSuchMethod( - Invocation.getter(#lastUnlocked), - returnValue: 0, - ) as int); - @override - set lastUnlocked(int? lastUnlocked) => super.noSuchMethod( - Invocation.setter( - #lastUnlocked, - lastUnlocked, - ), - returnValueForMissingStub: null, - ); - @override - int get currentNotificationId => (super.noSuchMethod( - Invocation.getter(#currentNotificationId), - returnValue: 0, - ) as int); - @override - List get walletIdsSyncOnStartup => (super.noSuchMethod( - Invocation.getter(#walletIdsSyncOnStartup), - returnValue: [], - ) as List); - @override - set walletIdsSyncOnStartup(List? walletIdsSyncOnStartup) => - super.noSuchMethod( - Invocation.setter( - #walletIdsSyncOnStartup, - walletIdsSyncOnStartup, - ), - returnValueForMissingStub: null, - ); - @override - _i7.SyncingType get syncType => (super.noSuchMethod( - Invocation.getter(#syncType), - returnValue: _i7.SyncingType.currentWalletOnly, - ) as _i7.SyncingType); - @override - set syncType(_i7.SyncingType? syncType) => super.noSuchMethod( - Invocation.setter( - #syncType, - syncType, - ), - returnValueForMissingStub: null, - ); - @override - bool get wifiOnly => (super.noSuchMethod( - Invocation.getter(#wifiOnly), - returnValue: false, - ) as bool); - @override - set wifiOnly(bool? wifiOnly) => super.noSuchMethod( - Invocation.setter( - #wifiOnly, - wifiOnly, - ), - returnValueForMissingStub: null, - ); - @override - bool get showFavoriteWallets => (super.noSuchMethod( - Invocation.getter(#showFavoriteWallets), - returnValue: false, - ) as bool); - @override - set showFavoriteWallets(bool? showFavoriteWallets) => super.noSuchMethod( - Invocation.setter( - #showFavoriteWallets, - showFavoriteWallets, - ), - returnValueForMissingStub: null, - ); - @override - String get language => (super.noSuchMethod( - Invocation.getter(#language), - returnValue: '', - ) as String); - @override - set language(String? newLanguage) => super.noSuchMethod( - Invocation.setter( - #language, - newLanguage, - ), - returnValueForMissingStub: null, - ); - @override - String get currency => (super.noSuchMethod( - Invocation.getter(#currency), - returnValue: '', - ) as String); - @override - set currency(String? newCurrency) => super.noSuchMethod( - Invocation.setter( - #currency, - newCurrency, - ), - returnValueForMissingStub: null, - ); - @override - bool get randomizePIN => (super.noSuchMethod( - Invocation.getter(#randomizePIN), - returnValue: false, - ) as bool); - @override - set randomizePIN(bool? randomizePIN) => super.noSuchMethod( - Invocation.setter( - #randomizePIN, - randomizePIN, - ), - returnValueForMissingStub: null, - ); - @override - bool get useBiometrics => (super.noSuchMethod( - Invocation.getter(#useBiometrics), - returnValue: false, - ) as bool); - @override - set useBiometrics(bool? useBiometrics) => super.noSuchMethod( - Invocation.setter( - #useBiometrics, - useBiometrics, - ), - returnValueForMissingStub: null, - ); - @override - bool get hasPin => (super.noSuchMethod( - Invocation.getter(#hasPin), - returnValue: false, - ) as bool); - @override - set hasPin(bool? hasPin) => super.noSuchMethod( - Invocation.setter( - #hasPin, - hasPin, - ), - returnValueForMissingStub: null, - ); - @override - int get familiarity => (super.noSuchMethod( - Invocation.getter(#familiarity), - returnValue: 0, - ) as int); - @override - set familiarity(int? familiarity) => super.noSuchMethod( - Invocation.setter( - #familiarity, - familiarity, - ), - returnValueForMissingStub: null, - ); - @override - bool get torKillSwitch => (super.noSuchMethod( - Invocation.getter(#torKillSwitch), - returnValue: false, - ) as bool); - @override - set torKillSwitch(bool? torKillswitch) => super.noSuchMethod( - Invocation.setter( - #torKillSwitch, - torKillswitch, - ), - returnValueForMissingStub: null, - ); - @override - bool get showTestNetCoins => (super.noSuchMethod( - Invocation.getter(#showTestNetCoins), - returnValue: false, - ) as bool); - @override - set showTestNetCoins(bool? showTestNetCoins) => super.noSuchMethod( - Invocation.setter( - #showTestNetCoins, - showTestNetCoins, - ), - returnValueForMissingStub: null, - ); - @override - bool get isAutoBackupEnabled => (super.noSuchMethod( - Invocation.getter(#isAutoBackupEnabled), - returnValue: false, - ) as bool); - @override - set isAutoBackupEnabled(bool? isAutoBackupEnabled) => super.noSuchMethod( - Invocation.setter( - #isAutoBackupEnabled, - isAutoBackupEnabled, - ), - returnValueForMissingStub: null, - ); - @override - set autoBackupLocation(String? autoBackupLocation) => super.noSuchMethod( - Invocation.setter( - #autoBackupLocation, - autoBackupLocation, - ), - returnValueForMissingStub: null, - ); - @override - _i8.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( - Invocation.getter(#backupFrequencyType), - returnValue: _i8.BackupFrequencyType.everyTenMinutes, - ) as _i8.BackupFrequencyType); - @override - set backupFrequencyType(_i8.BackupFrequencyType? backupFrequencyType) => - super.noSuchMethod( - Invocation.setter( - #backupFrequencyType, - backupFrequencyType, - ), - returnValueForMissingStub: null, - ); - @override - set lastAutoBackup(DateTime? lastAutoBackup) => super.noSuchMethod( - Invocation.setter( - #lastAutoBackup, - lastAutoBackup, - ), - returnValueForMissingStub: null, - ); - @override - bool get hideBlockExplorerWarning => (super.noSuchMethod( - Invocation.getter(#hideBlockExplorerWarning), - returnValue: false, - ) as bool); - @override - set hideBlockExplorerWarning(bool? hideBlockExplorerWarning) => - super.noSuchMethod( - Invocation.setter( - #hideBlockExplorerWarning, - hideBlockExplorerWarning, - ), - returnValueForMissingStub: null, - ); - @override - bool get gotoWalletOnStartup => (super.noSuchMethod( - Invocation.getter(#gotoWalletOnStartup), - returnValue: false, - ) as bool); - @override - set gotoWalletOnStartup(bool? gotoWalletOnStartup) => super.noSuchMethod( - Invocation.setter( - #gotoWalletOnStartup, - gotoWalletOnStartup, - ), - returnValueForMissingStub: null, - ); - @override - set startupWalletId(String? startupWalletId) => super.noSuchMethod( - Invocation.setter( - #startupWalletId, - startupWalletId, - ), - returnValueForMissingStub: null, - ); - @override - bool get externalCalls => (super.noSuchMethod( - Invocation.getter(#externalCalls), - returnValue: false, - ) as bool); - @override - set externalCalls(bool? externalCalls) => super.noSuchMethod( - Invocation.setter( - #externalCalls, - externalCalls, - ), - returnValueForMissingStub: null, - ); - @override - bool get enableCoinControl => (super.noSuchMethod( - Invocation.getter(#enableCoinControl), - returnValue: false, - ) as bool); - @override - set enableCoinControl(bool? enableCoinControl) => super.noSuchMethod( - Invocation.setter( - #enableCoinControl, - enableCoinControl, - ), - returnValueForMissingStub: null, - ); - @override - bool get enableSystemBrightness => (super.noSuchMethod( - Invocation.getter(#enableSystemBrightness), - returnValue: false, - ) as bool); - @override - set enableSystemBrightness(bool? enableSystemBrightness) => - super.noSuchMethod( - Invocation.setter( - #enableSystemBrightness, - enableSystemBrightness, - ), - returnValueForMissingStub: null, - ); - @override - String get themeId => (super.noSuchMethod( - Invocation.getter(#themeId), - returnValue: '', - ) as String); - @override - set themeId(String? themeId) => super.noSuchMethod( - Invocation.setter( - #themeId, - themeId, - ), - returnValueForMissingStub: null, - ); - @override - String get systemBrightnessLightThemeId => (super.noSuchMethod( - Invocation.getter(#systemBrightnessLightThemeId), - returnValue: '', - ) as String); - @override - set systemBrightnessLightThemeId(String? systemBrightnessLightThemeId) => - super.noSuchMethod( - Invocation.setter( - #systemBrightnessLightThemeId, - systemBrightnessLightThemeId, - ), - returnValueForMissingStub: null, - ); - @override - String get systemBrightnessDarkThemeId => (super.noSuchMethod( - Invocation.getter(#systemBrightnessDarkThemeId), - returnValue: '', - ) as String); - @override - set systemBrightnessDarkThemeId(String? systemBrightnessDarkThemeId) => - super.noSuchMethod( - Invocation.setter( - #systemBrightnessDarkThemeId, - systemBrightnessDarkThemeId, - ), - returnValueForMissingStub: null, - ); - @override - bool get useTor => (super.noSuchMethod( - Invocation.getter(#useTor), - returnValue: false, - ) as bool); - @override - set useTor(bool? useTor) => super.noSuchMethod( - Invocation.setter( - #useTor, - useTor, - ), - returnValueForMissingStub: null, - ); - @override - bool get hasListeners => (super.noSuchMethod( - Invocation.getter(#hasListeners), - returnValue: false, - ) as bool); - @override - _i5.Future init() => (super.noSuchMethod( - Invocation.method( - #init, - [], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( - Invocation.method( - #incrementCurrentNotificationIndex, - [], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future isExternalCallsSet() => (super.noSuchMethod( - Invocation.method( - #isExternalCallsSet, - [], - ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); - @override - _i5.Future saveUserID(String? userId) => (super.noSuchMethod( - Invocation.method( - #saveUserID, - [userId], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( - Invocation.method( - #saveSignupEpoch, - [signupEpoch], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i9.AmountUnit amountUnit(_i10.Coin? coin) => (super.noSuchMethod( - Invocation.method( - #amountUnit, - [coin], - ), - returnValue: _i9.AmountUnit.normal, - ) as _i9.AmountUnit); - @override - void updateAmountUnit({ - required _i10.Coin? coin, - required _i9.AmountUnit? amountUnit, - }) => - super.noSuchMethod( - Invocation.method( - #updateAmountUnit, - [], - { - #coin: coin, - #amountUnit: amountUnit, - }, - ), - returnValueForMissingStub: null, - ); - @override - int maxDecimals(_i10.Coin? coin) => (super.noSuchMethod( - Invocation.method( - #maxDecimals, - [coin], - ), - returnValue: 0, - ) as int); - @override - void updateMaxDecimals({ - required _i10.Coin? coin, - required int? maxDecimals, - }) => - super.noSuchMethod( - Invocation.method( - #updateMaxDecimals, - [], - { - #coin: coin, - #maxDecimals: maxDecimals, - }, - ), - returnValueForMissingStub: null, - ); - @override - _i3.FusionInfo getFusionServerInfo(_i10.Coin? coin) => (super.noSuchMethod( - Invocation.method( - #getFusionServerInfo, - [coin], - ), - returnValue: _FakeFusionInfo_2( - this, - Invocation.method( - #getFusionServerInfo, - [coin], - ), - ), - ) as _i3.FusionInfo); - @override - void setFusionServerInfo( - _i10.Coin? coin, - _i3.FusionInfo? fusionServerInfo, - ) => - super.noSuchMethod( - Invocation.method( - #setFusionServerInfo, - [ - coin, - fusionServerInfo, - ], - ), - returnValueForMissingStub: null, - ); - @override - void addListener(_i11.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - ), - returnValueForMissingStub: null, - ); - @override - void removeListener(_i11.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #removeListener, - [listener], - ), - returnValueForMissingStub: null, - ); - @override - void dispose() => super.noSuchMethod( - Invocation.method( - #dispose, - [], - ), - returnValueForMissingStub: null, - ); - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method( - #notifyListeners, - [], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [TorService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTorService extends _i1.Mock implements _i12.TorService { - MockTorService() { - _i1.throwOnMissingStub(this); - } - - @override - _i13.TorConnectionStatus get status => (super.noSuchMethod( - Invocation.getter(#status), - returnValue: _i13.TorConnectionStatus.disconnected, - ) as _i13.TorConnectionStatus); - @override - ({_i4.InternetAddress host, int port}) getProxyInfo() => (super.noSuchMethod( - Invocation.method( - #getProxyInfo, - [], - ), - returnValue: ( - host: _FakeInternetAddress_3( - this, - Invocation.method( - #getProxyInfo, - [], - ), - ), - port: 0 - ), - ) as ({_i4.InternetAddress host, int port})); - @override - void init({ - required String? torDataDirPath, - _i14.Tor? mockableOverride, - }) => - super.noSuchMethod( - Invocation.method( - #init, - [], - { - #torDataDirPath: torDataDirPath, - #mockableOverride: mockableOverride, - }, - ), - returnValueForMissingStub: null, - ); - @override - _i5.Future start() => (super.noSuchMethod( - Invocation.method( - #start, - [], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override - _i5.Future disable() => (super.noSuchMethod( - Invocation.method( - #disable, - [], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); -} diff --git a/test/json_rpc_test.dart b/test/json_rpc_test.dart deleted file mode 100644 index e9da77971..000000000 --- a/test/json_rpc_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:stackwallet/electrumx_rpc/rpc.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; - -void main() { - test("REQUIRES INTERNET - JsonRPC.request success", () async { - final jsonRPC = JsonRPC( - host: DefaultNodes.bitcoin.host, - port: DefaultNodes.bitcoin.port, - useSSL: true, - connectionTimeout: const Duration(seconds: 40), - proxyInfo: null, // TODO test for proxyInfo - ); - - const jsonRequestString = - '{"jsonrpc": "2.0", "id": "some id","method": "server.ping","params": []}'; - final result = await jsonRPC.request( - jsonRequestString, - const Duration(seconds: 1), - ); - - expect(result.data, {"jsonrpc": "2.0", "result": null, "id": "some id"}); - }); - - test("JsonRPC.request fails due to SocketException", () async { - final jsonRPC = JsonRPC( - host: "some.bad.address.thingdsfsdfsdaf", - port: 3000, - connectionTimeout: const Duration(seconds: 10), - proxyInfo: null, - ); - - const jsonRequestString = - '{"jsonrpc": "2.0", "id": "some id","method": "server.ping","params": []}'; - - expect( - () => jsonRPC.request( - jsonRequestString, - const Duration(seconds: 1), - ), - throwsA(isA())); - }); - - test("JsonRPC.request fails due to connection timeout", () async { - final jsonRPC = JsonRPC( - host: "8.8.8.8", - port: 3000, - useSSL: false, - connectionTimeout: const Duration(seconds: 1), - proxyInfo: null, - ); - - const jsonRequestString = - '{"jsonrpc": "2.0", "id": "some id","method": "server.ping","params": []}'; - - await expectLater( - jsonRPC.request( - jsonRequestString, - const Duration(seconds: 1), - ), - throwsA(isA() - .having((e) => e.toString(), 'message', contains("Request timeout"))), - ); - }); -} diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index bb7ed9b5b..5bc43e816 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -78,6 +78,7 @@ class MockCachedElectrumXClient extends _i1.Mock required String? groupId, String? blockhash = r'', required _i5.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -87,6 +88,7 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index f3b618de7..999e3f135 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -3,15 +3,17 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -24,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -34,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -44,9 +47,19 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -58,13 +71,21 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -88,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -109,7 +130,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future closeAdapter() => (super.noSuchMethod( + Invocation.method( + #closeAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +158,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +178,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +194,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +205,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +216,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +232,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +249,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +265,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +294,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +326,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +345,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +361,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +378,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +407,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +423,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +439,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +461,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -459,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -475,31 +504,31 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); + ) as _i4.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +541,14 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -528,11 +558,12 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +581,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +597,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +613,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +671,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +688,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index 3a2d12610..9886b150a 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -3,15 +3,17 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -24,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -34,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -44,9 +47,19 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -58,13 +71,21 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -88,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -109,7 +130,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future closeAdapter() => (super.noSuchMethod( + Invocation.method( + #closeAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +158,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +178,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +194,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +205,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +216,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +232,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +249,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +265,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +294,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +326,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +345,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +361,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +378,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +407,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +423,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +439,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +461,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -459,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -475,31 +504,31 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); + ) as _i4.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +541,14 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -528,11 +558,12 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +581,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +597,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +613,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +671,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +688,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 87341f635..f9cc4af74 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -3,15 +3,17 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -24,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -34,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -44,9 +47,19 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -58,13 +71,21 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -88,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -109,7 +130,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future closeAdapter() => (super.noSuchMethod( + Invocation.method( + #closeAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +158,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +178,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +194,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +205,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +216,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +232,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +249,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +265,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +294,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +326,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +345,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +361,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +378,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +407,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +423,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +439,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +461,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -459,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -475,31 +504,31 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); + ) as _i4.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +541,14 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -528,11 +558,12 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +581,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +597,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +613,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +671,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +688,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index 1102ebf10..c5ed61ed5 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -3,15 +3,17 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -24,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -34,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -44,9 +47,19 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -58,13 +71,21 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -88,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -109,7 +130,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future closeAdapter() => (super.noSuchMethod( + Invocation.method( + #closeAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +158,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +178,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +194,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +205,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +216,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +232,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +249,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +265,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +294,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +326,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +345,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +361,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +378,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +407,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +423,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +439,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +461,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -459,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -475,31 +504,31 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); + ) as _i4.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +541,14 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -528,11 +558,12 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +581,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +597,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +613,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +671,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +688,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index b8ef7a694..587dbe42e 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -3,15 +3,17 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; -import 'package:decimal/decimal.dart' as _i2; +import 'package:decimal/decimal.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i5; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i3; +import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i6; +import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i4; import 'package:stackwallet/services/transaction_notification_tracker.dart' - as _i7; -import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; + as _i8; +import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i7; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' + as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -24,8 +26,9 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart' as _i6; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeDuration_0 extends _i1.SmartFake implements Duration { - _FakeDuration_0( +class _FakeCryptoCurrency_0 extends _i1.SmartFake + implements _i2.CryptoCurrency { + _FakeCryptoCurrency_0( Object parent, Invocation parentInvocation, ) : super( @@ -34,8 +37,8 @@ class _FakeDuration_0 extends _i1.SmartFake implements Duration { ); } -class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { - _FakeDecimal_1( +class _FakeDuration_1 extends _i1.SmartFake implements Duration { + _FakeDuration_1( Object parent, Invocation parentInvocation, ) : super( @@ -44,9 +47,19 @@ class _FakeDecimal_1 extends _i1.SmartFake implements _i2.Decimal { ); } -class _FakeElectrumXClient_2 extends _i1.SmartFake - implements _i3.ElectrumXClient { - _FakeElectrumXClient_2( +class _FakeDecimal_2 extends _i1.SmartFake implements _i3.Decimal { + _FakeDecimal_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeElectrumXClient_3 extends _i1.SmartFake + implements _i4.ElectrumXClient { + _FakeElectrumXClient_3( Object parent, Invocation parentInvocation, ) : super( @@ -58,13 +71,21 @@ class _FakeElectrumXClient_2 extends _i1.SmartFake /// A class which mocks [ElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { +class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { MockElectrumXClient() { _i1.throwOnMissingStub(this); } @override - set failovers(List<_i3.ElectrumXNode>? _failovers) => super.noSuchMethod( + _i2.CryptoCurrency get cryptoCurrency => (super.noSuchMethod( + Invocation.getter(#cryptoCurrency), + returnValue: _FakeCryptoCurrency_0( + this, + Invocation.getter(#cryptoCurrency), + ), + ) as _i2.CryptoCurrency); + @override + set failovers(List<_i4.ElectrumXNode>? _failovers) => super.noSuchMethod( Invocation.setter( #failovers, _failovers, @@ -88,7 +109,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { Duration get connectionTimeoutForSpecialCaseJsonRPCClients => (super.noSuchMethod( Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), - returnValue: _FakeDuration_0( + returnValue: _FakeDuration_1( this, Invocation.getter(#connectionTimeoutForSpecialCaseJsonRPCClients), ), @@ -109,7 +130,16 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { returnValue: false, ) as bool); @override - _i4.Future request({ + _i5.Future closeAdapter() => (super.noSuchMethod( + Invocation.method( + #closeAdapter, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future request({ required String? command, List? args = const [], String? requestID, @@ -128,12 +158,12 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestTimeout: requestTimeout, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future>> batchRequest({ + _i5.Future> batchRequest({ required String? command, - required Map>? args, + required List? args, Duration? requestTimeout = const Duration(seconds: 60), int? retries = 2, }) => @@ -148,11 +178,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retries: retries, }, ), - returnValue: _i4.Future>>.value( - >[]), - ) as _i4.Future>>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future ping({ + _i5.Future ping({ String? requestID, int? retryCount = 1, }) => @@ -165,10 +194,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #retryCount: retryCount, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getBlockHeadTip({String? requestID}) => + _i5.Future> getBlockHeadTip({String? requestID}) => (super.noSuchMethod( Invocation.method( #getBlockHeadTip, @@ -176,10 +205,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getServerFeatures({String? requestID}) => + _i5.Future> getServerFeatures({String? requestID}) => (super.noSuchMethod( Invocation.method( #getServerFeatures, @@ -187,10 +216,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future broadcastTransaction({ + _i5.Future broadcastTransaction({ required String? rawTx, String? requestID, }) => @@ -203,10 +232,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(''), - ) as _i4.Future); + returnValue: _i5.Future.value(''), + ) as _i5.Future); @override - _i4.Future> getBalance({ + _i5.Future> getBalance({ required String? scripthash, String? requestID, }) => @@ -220,10 +249,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getHistory({ + _i5.Future>> getHistory({ required String? scripthash, String? requestID, }) => @@ -236,23 +265,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchHistory( - {required Map>? args}) => + _i5.Future>>> getBatchHistory( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchHistory, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future>> getUTXOs({ + _i5.Future>> getUTXOs({ required String? scripthash, String? requestID, }) => @@ -265,23 +294,23 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future>>> getBatchUTXOs( - {required Map>? args}) => + _i5.Future>>> getBatchUTXOs( + {required List? args}) => (super.noSuchMethod( Invocation.method( #getBatchUTXOs, [], {#args: args}, ), - returnValue: _i4.Future>>>.value( - >>{}), - ) as _i4.Future>>>); + returnValue: _i5.Future>>>.value( + >>[]), + ) as _i5.Future>>>); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, bool? verbose = true, String? requestID, @@ -297,10 +326,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getLelantusAnonymitySet({ + _i5.Future> getLelantusAnonymitySet({ String? groupId = r'1', String? blockhash = r'', String? requestID, @@ -316,10 +345,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusMintData({ + _i5.Future getLelantusMintData({ dynamic mints, String? requestID, }) => @@ -332,10 +361,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #requestID: requestID, }, ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future> getLelantusUsedCoinSerials({ + _i5.Future> getLelantusUsedCoinSerials({ String? requestID, required int? startNumber, }) => @@ -349,20 +378,20 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future getLelantusLatestCoinId({String? requestID}) => + _i5.Future getLelantusLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getLelantusLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ String? coinGroupId = r'1', String? startBlockHash = r'', String? requestID, @@ -378,10 +407,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({ + _i5.Future> getSparkUsedCoinsTags({ String? requestID, required int? startNumber, }) => @@ -394,10 +423,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future>> getSparkMintMetaData({ + _i5.Future>> getSparkMintMetaData({ String? requestID, required List? sparkCoinHashes, }) => @@ -410,21 +439,21 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #sparkCoinHashes: sparkCoinHashes, }, ), - returnValue: _i4.Future>>.value( + returnValue: _i5.Future>>.value( >[]), - ) as _i4.Future>>); + ) as _i5.Future>>); @override - _i4.Future getSparkLatestCoinId({String? requestID}) => + _i5.Future getSparkLatestCoinId({String? requestID}) => (super.noSuchMethod( Invocation.method( #getSparkLatestCoinId, [], {#requestID: requestID}, ), - returnValue: _i4.Future.value(0), - ) as _i4.Future); + returnValue: _i5.Future.value(0), + ) as _i5.Future); @override - _i4.Future> getFeeRate({String? requestID}) => + _i5.Future> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( #getFeeRate, @@ -432,10 +461,10 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future<_i2.Decimal> estimateFee({ + _i5.Future<_i3.Decimal> estimateFee({ String? requestID, required int? blocks, }) => @@ -448,7 +477,7 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { #blocks: blocks, }, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #estimateFee, @@ -459,15 +488,15 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { }, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); @override - _i4.Future<_i2.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( + _i5.Future<_i3.Decimal> relayFee({String? requestID}) => (super.noSuchMethod( Invocation.method( #relayFee, [], {#requestID: requestID}, ), - returnValue: _i4.Future<_i2.Decimal>.value(_FakeDecimal_1( + returnValue: _i5.Future<_i3.Decimal>.value(_FakeDecimal_2( this, Invocation.method( #relayFee, @@ -475,31 +504,31 @@ class MockElectrumXClient extends _i1.Mock implements _i3.ElectrumXClient { {#requestID: requestID}, ), )), - ) as _i4.Future<_i2.Decimal>); + ) as _i5.Future<_i3.Decimal>); } /// A class which mocks [CachedElectrumXClient]. /// /// See the documentation for Mockito's code generation for more information. class MockCachedElectrumXClient extends _i1.Mock - implements _i5.CachedElectrumXClient { + implements _i6.CachedElectrumXClient { MockCachedElectrumXClient() { _i1.throwOnMissingStub(this); } @override - _i3.ElectrumXClient get electrumXClient => (super.noSuchMethod( + _i4.ElectrumXClient get electrumXClient => (super.noSuchMethod( Invocation.getter(#electrumXClient), - returnValue: _FakeElectrumXClient_2( + returnValue: _FakeElectrumXClient_3( this, Invocation.getter(#electrumXClient), ), - ) as _i3.ElectrumXClient); + ) as _i4.ElectrumXClient); @override - _i4.Future> getAnonymitySet({ + _i5.Future> getAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, }) => (super.noSuchMethod( Invocation.method( @@ -512,13 +541,14 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getSparkAnonymitySet({ + _i5.Future> getSparkAnonymitySet({ required String? groupId, String? blockhash = r'', - required _i6.Coin? coin, + required _i7.Coin? coin, + required bool? useOnlyCacheIfNotEmpty, }) => (super.noSuchMethod( Invocation.method( @@ -528,11 +558,12 @@ class MockCachedElectrumXClient extends _i1.Mock #groupId: groupId, #blockhash: blockhash, #coin: coin, + #useOnlyCacheIfNotEmpty: useOnlyCacheIfNotEmpty, }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -550,9 +581,9 @@ class MockCachedElectrumXClient extends _i1.Mock returnValue: '', ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i6.Coin? coin, + required _i7.Coin? coin, bool? verbose = true, }) => (super.noSuchMethod( @@ -566,11 +597,11 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future> getUsedCoinSerials({ - required _i6.Coin? coin, + _i5.Future> getUsedCoinSerials({ + required _i7.Coin? coin, int? startNumber = 0, }) => (super.noSuchMethod( @@ -582,36 +613,36 @@ class MockCachedElectrumXClient extends _i1.Mock #startNumber: startNumber, }, ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); @override - _i4.Future> getSparkUsedCoinsTags({required _i6.Coin? coin}) => + _i5.Future> getSparkUsedCoinsTags({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #getSparkUsedCoinsTags, [], {#coin: coin}, ), - returnValue: _i4.Future>.value({}), - ) as _i4.Future>); + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache({required _i6.Coin? coin}) => + _i5.Future clearSharedTransactionCache({required _i7.Coin? coin}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#coin: coin}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TransactionNotificationTracker]. /// /// See the documentation for Mockito's code generation for more information. class MockTransactionNotificationTracker extends _i1.Mock - implements _i7.TransactionNotificationTracker { + implements _i8.TransactionNotificationTracker { MockTransactionNotificationTracker() { _i1.throwOnMissingStub(this); } @@ -640,14 +671,14 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedPending(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedPending(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedPending, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override bool wasNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( @@ -657,21 +688,21 @@ class MockTransactionNotificationTracker extends _i1.Mock returnValue: false, ) as bool); @override - _i4.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( + _i5.Future addNotifiedConfirmed(String? txid) => (super.noSuchMethod( Invocation.method( #addNotifiedConfirmed, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future deleteTransaction(String? txid) => (super.noSuchMethod( + _i5.Future deleteTransaction(String? txid) => (super.noSuchMethod( Invocation.method( #deleteTransaction, [txid], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/windows/.gitignore b/windows/.gitignore index d492d0d98..57538caed 100644 --- a/windows/.gitignore +++ b/windows/.gitignore @@ -1,4 +1,4 @@ -flutter/ephemeral/ +flutter/ephemeral # Visual Studio user-specific files. *.suo diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d2071a..903f4899d 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a774c684a..02d70698f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -18,6 +18,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST coinlib_flutter flutter_libsparkmobile + frostdart tor_ffi_plugin )