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