Merge pull request #866 from cypherstack/staging

Update to version 2.0.0
This commit is contained in:
Diego Salazar 2024-05-14 11:03:42 -06:00 committed by GitHub
commit f5489feae7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
242 changed files with 23464 additions and 8313 deletions

View file

@ -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/

5
.gitmodules vendored
View file

@ -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
url = https://github.com/cypherstack/flutter_liblelantus.git
[submodule "crypto_plugins/frostdart"]
path = crypto_plugins/frostdart
url = https://github.com/cypherstack/frostdart

View file

@ -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

View file

@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 33
compileSdkVersion 34
// ndkVersion = "21.1.6352462"
// ndkVersion = "25.2.9519653"

Binary file not shown.

Binary file not shown.

BIN
assets/images/mascot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

10
assets/svg/swap2.svg Normal file
View file

@ -0,0 +1,10 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_9616_26154)">
<path d="M11.2188 3.90625H3.625C2.38236 3.90625 1.375 4.91361 1.375 6.15625M11.2188 3.90625L8.96875 1.375M11.2188 3.90625L8.96875 6.4375M2.78125 10.375L10.375 10.375C11.6176 10.375 12.625 9.36764 12.625 8.125M2.78125 10.375L5.03125 12.9062M2.78125 10.375L5.03125 7.84375" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_9616_26154">
<rect width="13.5" height="13.5" fill="white" transform="translate(0.25 0.25)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 643 B

@ -1 +1 @@
Subproject commit cef5d3aa8c74c8dc9a466f803c964b243ad653a3
Subproject commit 19c76409e55f1bfed58855eb767574604376edb6

@ -1 +1 @@
Subproject commit 9cd241b5ea142e21c01dd7639b42603281c43287
Subproject commit b654bf4488357c8a104900e11f9468d54a39f22b

@ -1 +1 @@
Subproject commit cb876251b97d20b12ddd05268913d2cf4b78f0bf
Subproject commit 2c684cedba6c3d9353c7ea748cadb5a246008027

@ -0,0 +1 @@
Subproject commit d539de2348bdbb87bac341dcaa6a0755f21d48e2

View file

@ -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.*'
```
<!-- TODO: configure compiler to prefer built over system libraries. Should already use them? -->
### 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.*'
```
<!-- TODO: configure compiler to prefer built over system libraries. Should already use them? -->
#### 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
```
<!-- TODO: determine which of the above list are not needed at all. -->
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:
-->
<!-- TODO: script the copying or installation of libraries from WSL2 to the parent 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
```
<!--
You may also need to install `cargo-ndk`:
```
rustup install 1.73.0 # For cargo-ndk.
cargo install cargo-ndk --version 2.12.7 --locked
```
-->
### 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.

View file

@ -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

View file

@ -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",

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -33,6 +33,8 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:tuple/tuple.dart';
class DbVersionMigrator with WalletDB {
@ -85,7 +87,7 @@ class DbVersionMigrator with WalletDB {
useSSL: node.useSSL),
prefs: prefs,
failovers: failovers,
coin: Coin.firo,
cryptoCurrency: Firo(CryptoCurrencyNetwork.main),
);
try {

View file

@ -21,6 +21,7 @@ import 'package:stackwallet/models/isar/stack_theme.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/spark_coin.dart';
import 'package:stackwallet/wallets/isar/models/token_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
@ -67,6 +68,7 @@ class MainDB {
SparkCoinSchema,
WalletInfoMetaSchema,
TokenWalletInfoSchema,
FrostWalletInfoSchema,
],
directory: (await StackFileSystem.applicationIsarDirectory()).path,
// inspector: kDebugMode,

View file

@ -11,9 +11,6 @@
import 'dart:convert';
import 'dart:math';
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
import 'package:electrum_adapter/electrum_adapter.dart';
import 'package:electrum_adapter/methods/specific/firo.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -22,41 +19,18 @@ import 'package:string_validator/string_validator.dart';
class CachedElectrumXClient {
final ElectrumXClient electrumXClient;
ElectrumClient electrumAdapterClient;
final Future<ElectrumClient> 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<ElectrumClient> 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<Map<String, dynamic>> getAnonymitySet({
required String groupId,
String blockhash = "",
@ -80,12 +54,9 @@ class CachedElectrumXClient {
set = Map<String, dynamic>.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<String, dynamic>.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<String, dynamic> 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<String>.from(serials["serials"] as List)
// .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e)
// .toSet();
// Convert the Map<String, dynamic> tags to a Set<Object?>.
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);
}

View file

@ -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<String, ElectrumClient> _map = {};
final Map<String, int> _heights = {};
final Map<String, StreamSubscription<BlockHeader>> _subscriptions = {};
final Map<String, Completer<int>> _heightCompleters = {};
String _keyHelper(CryptoCurrency cryptoCurrency) {
return "${cryptoCurrency.runtimeType}_${cryptoCurrency.network.name}";
}
final Finalizer<ClientManager> _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<int>();
_subscriptions[key] = client.subscribeHeaders().listen((event) {
_heights[key] = event.height;
if (!_heightCompleters[key]!.isCompleted) {
_heightCompleters[key]!.complete(event.height);
}
});
}
Future<int> 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<ElectrumClient?> 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<void> closeAll() async {
await _kill();
_finalizer.detach(this);
}
Future<void> _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();
}
}

View file

@ -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<Coin, ChainHeightService> _services = {};
// Map<Coin, ChainHeightService> 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<void> dispose() async {
// Close each subscription.
//
// Create a list of keys to avoid concurrent modification during iteration
var keys = List<Coin>.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<dynamic>? _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<int> 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<int>();
// 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<void> cancelListen() async {
await _subscription?.cancel();
_subscription = null;
_reconnectTimer?.cancel();
}
}

View file

@ -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<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
// StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
StreamChannel<dynamic>? _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<ElectrumXClient> _finalizer = Finalizer(
@ -114,7 +113,7 @@ class ElectrumXClient {
required bool useSSL,
required Prefs prefs,
required List<ElectrumXNode> 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<ElectrumXNode> 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<void> checkElectrumAdapter() async {
Future<void> closeAdapter() async {
await getElectrumAdapter()?.close();
}
Future<void> _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 = <Future<dynamic>>[];
_electrumAdapterClient!.peer.withBatch(() {
final futures = <Future<dynamic>>[];
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<String, dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)!
Logging.instance.log(
"attempting to fetch lelantus.getanonymityset...",
level: LogLevel.Info,
);
await _checkElectrumAdapter();
final Map<String, dynamic> 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<int> 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<String, dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
Logging.instance.log(
"attempting to fetch spark.getsparkanonymityset...",
level: LogLevel.Info,
);
await _checkElectrumAdapter();
final Map<String, dynamic> 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<String, dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
Logging.instance.log(
"attempting to fetch spark.getusedcoinstags...",
level: LogLevel.Info,
);
await _checkElectrumAdapter();
final Map<String, dynamic> 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<String, dynamic>.from(response);
final set = Set<String>.from(map["tags"] as List);
return await compute(_ffiHashTagsComputeWrapper, set);
@ -955,14 +979,18 @@ class ElectrumXClient {
required List<String> sparkCoinHashes,
}) async {
try {
Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...",
level: LogLevel.Info);
await checkElectrumAdapter();
List<dynamic> response =
await (_electrumAdapterClient as FiroElectrumClient)
Logging.instance.log(
"attempting to fetch spark.getsparkmintmetadata...",
level: LogLevel.Info,
);
await _checkElectrumAdapter();
final List<dynamic> 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<Map<String, dynamic>>.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<Map<String, dynamic>> 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);
}

View file

@ -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<List<int>>? _subscription;
void _dataHandler(List<int> 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<JsonRPCResponse> 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<JsonRPCResponse>(),
);
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<void> disconnect({
required String reason,
bool ignoreMutex = false,
}) async {
if (ignoreMutex) {
await _disconnectHelper(reason: reason);
} else {
await _requestMutex.protect(() async {
await _disconnectHelper(reason: reason);
});
}
}
Future<void> _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<void> _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<void> add(
_JsonRPCRequest req, {
VoidCallback? onInitialRequestAdded,
}) async {
return await _lock.protect(() async {
_rq.add(req);
if (_rq.length == 1) {
onInitialRequestAdded?.call();
}
});
}
Future<bool> 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<void> 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<bool> 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<JsonRPCResponse> completer;
final Duration requestTimeout;
final List<int> _responseData = [];
_JsonRPCRequest({
required this.jsonRequest,
required this.completer,
required this.requestTimeout,
});
void appendDataAndCheckIfComplete(List<int> 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<void>.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});
}

View file

@ -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<FrostStepRoute> stepRoutes,
FrostInterruptionDialogType frostInterruptionDialogType,
NavigatorState parentNav,
String callerRouteName,
})?>((ref) => null);
abstract class FrostRouteGenerator {
static const bool useMaterialPageRoute = true;
static const List<FrostStepRoute> 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<FrostStepRoute> 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<FrostStepRoute> 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<FrostStepRoute> 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<FrostStepRoute> 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<FrostStepRoute> 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<FrostStepRoute> 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<dynamic> 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<dynamic> _routeError(String message) {
return RouteGenerator.getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => Placeholder(
child: Center(
child: Text(message),
),
),
);
}
}

View file

@ -687,6 +687,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
appBarTheme: AppBarTheme(
centerTitle: false,
color: colorScheme.background,
surfaceTintColor: colorScheme.background,
elevation: 0,
),
inputDecorationTheme: InputDecorationTheme(

View file

@ -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)";
}
}
}

View file

@ -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) {

View file

@ -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"
"}";
}
}

View file

@ -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<AddWalletView> {
_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) {

View file

@ -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<CreateNewFrostMsWalletView> createState() =>
_NewFrostMsWalletViewState();
}
class _NewFrostMsWalletViewState
extends ConsumerState<CreateNewFrostMsWalletView> {
final _thresholdController = TextEditingController();
final _participantsController = TextEditingController();
final List<TextEditingController> 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<void>(
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<StackColors>()!.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<StackColors>()!.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<StackColors>()!.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<StackColors>()!.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<StackColors>()!.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<StackColors>()!.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<void>(
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,
);
},
),
],
),
),
);
}
}

View file

@ -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<SelectNewFrostImportTypeView> createState() =>
_SelectNewFrostImportTypeViewState();
}
class _SelectNewFrostImportTypeViewState
extends ConsumerState<SelectNewFrostImportTypeView> {
_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<StackColors>()!.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<StackColors>()!
.topNavIconPrimary,
BlendMode.srcIn,
),
),
onPressed: () async {
await showDialog<void>(
context: context,
builder: (_) => const _FrostJoinInfoDialog(),
);
},
),
),
],
),
body: Container(
color: Theme.of(context).extension<StackColors>()!.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<StackColors>()!
.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<StackColors>()!
.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),
),
],
),
);
}
}

View file

@ -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<FrostCreateStep1a> createState() => _FrostCreateStep1aState();
}
class _FrostCreateStep1aState extends ConsumerState<FrostCreateStep1a> {
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<void>(
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<StackColors>()!.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<StackColors>()!.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<StackColors>()!
.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<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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,
);
},
),
],
),
);
}
}

View file

@ -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<FrostCreateStep1b> createState() => _FrostCreateStep1bState();
}
class _FrostCreateStep1bState extends ConsumerState<FrostCreateStep1b> {
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<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Invalid config",
desktopPopRootNavigator: Util.isDesktop,
),
);
}
if (!Frost.getParticipants(multisigConfig: config)
.contains(myNameFieldController.text)) {
return await showDialog<void>(
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,
);
},
)
],
),
);
}
}

View file

@ -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<FrostCreateStep2> createState() => _FrostCreateStep2State();
}
class _FrostCreateStep2State extends ConsumerState<FrostCreateStep2> {
static const info = [
"Share your commitment with other group members.",
"Enter their commitments into the corresponding fields.",
];
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<String> participants;
late final String myCommitment;
late final int myIndex;
final List<bool> 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<void>(
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<void>(
context: context,
builder: (_) => FrostErrorDialog(
title: "Failed to generate shares",
message: e.toString(),
),
);
}
}
},
),
],
),
);
}
}

View file

@ -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<FrostCreateStep3> createState() => _FrostCreateStep3State();
}
class _FrostCreateStep3State extends ConsumerState<FrostCreateStep3> {
static const info = [
"Send your share to other group members.",
"Enter their shares into the corresponding fields.",
];
bool _userVerifyContinue = false;
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<String> participants;
late final String myShare;
late final int myIndex;
final List<bool> 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<void>(
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<void>(
context: context,
builder: (_) => const FrostErrorDialog(
title: "Failed to complete key generation",
),
);
}
}
},
),
],
),
);
}
}

View file

@ -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<FrostCreateStep4> createState() => _FrostCreateStep4State();
}
class _FrostCreateStep4State extends ConsumerState<FrostCreateStep4> {
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,
);
},
)
],
),
);
}
}

View file

@ -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<FrostCreateStep5> createState() => _FrostCreateStep5State();
}
class _FrostCreateStep5State extends ConsumerState<FrostCreateStep5> {
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<StackColors>()!.warningBackground,
child: Text(
_warning,
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.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<dynamic>(
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;
}
},
),
],
),
);
}
}

View file

@ -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<FrostReshareStep1a> createState() => _FrostReshareStep1aState();
}
class _FrostReshareStep1aState extends ConsumerState<FrostReshareStep1a> {
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<void> _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<void>(
context: context,
builder: (_) => FrostErrorDialog(
title: e.toString(),
),
);
}
} finally {
_buttonLock = false;
}
}
void _showParticipantsDialog() {
final participants =
ref.read(pFrostResharingData).configData!.newParticipants;
showDialog<void>(
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<StackColors>()!.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<StackColors>()!.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<StackColors>()!
.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<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.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,
),
],
),
);
}
}

View file

@ -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<FrostReshareStep1b> createState() => _FrostReshareStep1bState();
}
class _FrostReshareStep1bState extends ConsumerState<FrostReshareStep1b> {
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<void> _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<void>(
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();
},
),
],
),
);
}
}

View file

@ -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<FrostReshareStep1c> createState() => _FrostReshareStep1cState();
}
class _FrostReshareStep1cState extends ConsumerState<FrostReshareStep1c> {
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<IncompleteFrostWallet> _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<void>(
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<void>(
context: context,
builder: (_) => StackOkDialog(
title: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
},
)
],
),
);
}
}

View file

@ -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<FrostReshareStep2abd> createState() =>
_FrostReshareStep2abdState();
}
class _FrostReshareStep2abdState extends ConsumerState<FrostReshareStep2abd> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final Map<String, int> resharers;
late final int myResharerIndexIndex;
late final String myResharerStart;
late final bool amOutgoingParticipant;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
bool _userVerifyContinue = false;
Future<void> _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<void>(
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,
),
],
),
);
}
}

View file

@ -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<FrostReshareStep2c> createState() => _FrostReshareStep2cState();
}
class _FrostReshareStep2cState extends ConsumerState<FrostReshareStep2c> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final Map<String, int> resharers;
final List<bool> fieldIsEmptyFlags = [];
bool _buttonLock = false;
Future<void> _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<void>(
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,
),
],
),
);
}
}

View file

@ -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<FrostReshareStep3abd> createState() =>
_FrostReshareStep3abdState();
}
class _FrostReshareStep3abdState extends ConsumerState<FrostReshareStep3abd> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final List<String> newParticipants;
late final int myIndex;
late final String? myEncryptionKey;
late final bool amOutgoingParticipant;
final List<bool> fieldIsEmptyFlags = [];
bool _userVerifyContinue = false;
bool _buttonLock = false;
Future<void> _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<void>(
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,
),
],
),
);
}
}

View file

@ -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<FrostReshareStep3c> createState() => _FrostReshareStep3cState();
}
class _FrostReshareStep3cState extends ConsumerState<FrostReshareStep3c> {
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,
);
},
),
],
),
);
}
}

View file

@ -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<FrostReshareStep4> createState() => _FrostReshareStep4State();
}
class _FrostReshareStep4State extends ConsumerState<FrostReshareStep4> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final Map<String, int> resharers;
late final String myName;
late final int? myResharerIndexIndex;
late final String? myResharerComplete;
late final bool amOutgoingParticipant;
late final bool amNewParticipant;
final List<bool> fieldIsEmptyFlags = [];
bool _userVerifyContinue = false;
bool _buttonLock = false;
Future<void> _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<void>(
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,
),
],
),
);
}
}

View file

@ -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<FrostReshareStep5> createState() => _FrostReshareStep5State();
}
class _FrostReshareStep5State extends ConsumerState<FrostReshareStep5> {
late final String config;
late final String serializedKeys;
late final String reshareId;
late final bool isNew;
bool _buttonLock = false;
Future<void> _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<void>(
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,
),
],
),
);
}
}

View file

@ -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<RestoreFrostMsWalletView> createState() =>
_RestoreFrostMsWalletViewState();
}
class _RestoreFrostMsWalletViewState
extends ConsumerState<RestoreFrostMsWalletView> {
late final TextEditingController keysFieldController, configFieldController;
late final FocusNode keysFocusNode, configFocusNode;
bool _keysEmpty = true, _configEmpty = true;
bool _restoreButtonLock = false;
Future<Wallet> _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<void> _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<void>(
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<StackColors>()!.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<StackColors>()!.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<void>.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,
),
],
),
),
);
}
}

View file

@ -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<NameYourWalletView> {
return name;
}
Future<void> _nextPressed() async {
final name = textEditingController.text;
if (mounted) {
// hide keyboard if has focus
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.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<NameYourWalletView> {
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<NameYourWalletView> {
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<NameYourWalletView> {
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<void>.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<StackColors>()!
.getPrimaryEnabledButtonStyle(context)
: Theme.of(context)
.extension<StackColors>()!
.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<StackColors>()!
.getPrimaryEnabledButtonStyle(context)
: Theme.of(context)
.extension<StackColors>()!
.getPrimaryDisabledButtonStyle(context),
child: Text(
"Next",
style: isDesktop
? _nextEnabled
? STextStyles.desktopButtonEnabled(context)
: STextStyles.desktopButtonDisabled(context)
: STextStyles.button(context),
),
),
),
),
if (isDesktop)
const Spacer(
flex: 15,

View file

@ -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<RestoreOptionsView> {
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<RestoreOptionsView> {
@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<RestoreOptionsView> {
super.dispose();
}
MaterialRoundedDatePickerStyle _buildDatePickerStyle() {
return MaterialRoundedDatePickerStyle(
paddingMonthHeader: const EdgeInsets.only(top: 11),
colorArrowNext: Theme.of(context).extension<StackColors>()!.textSubtitle1,
colorArrowPrevious:
Theme.of(context).extension<StackColors>()!.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<StackColors>()!.textSubtitle3,
),
textStyleDayOnCalendarSelected:
STextStyles.datePicker400(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.popupBG,
),
textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
textStyleYearButton: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textWhite,
),
textStyleButtonAction: GoogleFonts.inter(),
);
}
MaterialRoundedYearPickerStyle _buildYearPickerStyle() {
return MaterialRoundedYearPickerStyle(
textStyleYear: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle2,
),
textStyleYearSelected: STextStyles.datePicker600(context).copyWith(
fontSize: 18,
),
);
}
Future<void> nextPressed() async {
if (!isDesktop) {
// hide keyboard if has focus
@ -169,67 +119,23 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
}
Future<void> chooseDate() async {
final height = MediaQuery.of(context).size.height;
final fetchedColor =
Theme.of(context).extension<StackColors>()!.accentColorDark;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.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<void> chooseDesktopDate() async {
final height = MediaQuery.of(context).size.height;
final fetchedColor =
Theme.of(context).extension<StackColors>()!.accentColorDark;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.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);

View file

@ -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<RestoreWalletView> {
],
),
body: Container(
color: Theme.of(context).extension<StackColors>()!.background,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: SingleChildScrollView(
controller: controller,
child: Column(
children: [
/*if (isDesktop)
color: Theme.of(context).extension<StackColors>()!.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<StackColors>()!
.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<StackColors>()!
.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<StackColors>()!
.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<StackColors>()!
.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<StackColors>()!
.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<StackColors>()!
.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",
),
),
],
),
),
],
),
),
],
),
),
),
);
),
);
}
}

View file

@ -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<StackColors>()!.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,

View file

@ -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<AddressBookView> {
ref.refresh(addressBookFilterProvider);
if (widget.coin == null) {
List<Coin> coins = Coin.values.toList();
final List<Coin> 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<AddressBookView> {
}
WidgetsBinding.instance.addPostFrameCallback((_) async {
List<ContactAddressEntry> addresses = [];
final List<ContactAddressEntry> 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<AddressBookView> {
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<AddressBookView> {
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:

View file

@ -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<StackColors>()!
.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>()!

View file

@ -79,7 +79,7 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> {
Future<void>.delayed(const Duration(seconds: 2)),
]),
context: context,
isDesktop: Util.isDesktop,
rootNavigator: Util.isDesktop,
message: "Stopping fusion",
);

View file

@ -225,6 +225,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
builder: (context) {
return BuildingTransactionDialog(
coin: wallet.info.coin,
isSpark: wallet is FiroWallet && !firoPublicSend,
onCancel: () {
wasCancelled = true;
},
@ -249,7 +250,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
address: address,
amount: amount,
isChange: false,
)
),
],
note: "${model.trade!.payInCurrency.toUpperCase()}/"
"${model.trade!.payOutCurrency.toUpperCase()} exchange",
@ -472,10 +473,10 @@ class _Step4ViewState extends ConsumerState<Step4View> {
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<Step4View> {
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<Step4View> {
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<Step4View> {
GestureDetector(
onTap: () async {
final data = ClipboardData(
text: model.trade!.tradeId);
text: model.trade!.tradeId,
);
await clipboard.setData(data);
if (mounted) {
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
@ -689,9 +692,9 @@ class _Step4ViewState extends ConsumerState<Step4View> {
.infoItemIcons,
width: 12,
),
)
),
],
)
),
],
),
),
@ -739,7 +742,8 @@ class _Step4ViewState extends ConsumerState<Step4View> {
child: Text(
"Send ${model.sendTicker} to this address",
style: STextStyles.pageTitleH2(
context),
context,
),
),
),
const SizedBox(
@ -773,12 +777,13 @@ class _Step4ViewState extends ConsumerState<Step4View> {
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(
context),
context,
),
child: Text(
"Cancel",
style: STextStyles.button(
context)
.copyWith(
context,
).copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
@ -788,7 +793,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
),
),
],
)
),
],
),
);
@ -814,8 +819,9 @@ class _Step4ViewState extends ConsumerState<Step4View> {
final tuple = ref
.read(
exchangeSendFromWalletIdStateProvider
.state)
exchangeSendFromWalletIdStateProvider
.state,
)
.state;
if (tuple != null &&
model.sendTicker.toLowerCase() ==
@ -845,8 +851,8 @@ class _Step4ViewState extends ConsumerState<Step4View> {
(BuildContext context) {
final coin =
coinFromTickerCaseInsensitive(
model.trade!
.payInCurrency);
model.trade!.payInCurrency,
);
return SendFromView(
coin: coin,
amount: model.sendAmount
@ -868,7 +874,8 @@ class _Step4ViewState extends ConsumerState<Step4View> {
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(
context),
context,
),
child: Text(
buttonTitle,
style:

View file

@ -205,13 +205,13 @@ class _SendFromViewState extends ConsumerState<SendFromView> {
class SendFromCard extends ConsumerStatefulWidget {
const SendFromCard({
Key? key,
super.key,
required this.walletId,
required this.amount,
required this.address,
required this.trade,
this.fromDesktopStep4 = false,
}) : super(key: key);
});
final String walletId;
final Amount amount;
@ -235,6 +235,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
try {
bool wasCancelled = false;
final wallet = ref.read(pWallets).getWallet(walletId);
unawaited(
showDialog<dynamic>(
context: context,
@ -253,6 +255,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
),
child: BuildingTransactionDialog(
coin: coin,
isSpark:
wallet is FiroWallet && shouldSendPublicFiroFunds != true,
onCancel: () {
wasCancelled = true;
@ -273,8 +277,6 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
TxData txData;
Future<TxData> txDataFuture;
final wallet = ref.read(pWallets).getWallet(walletId);
// if not firo then do normal send
if (shouldSendPublicFiroFunds == null) {
final memo = coin == Coin.stellar || coin == Coin.stellarTestnet
@ -371,38 +373,38 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
}
}
} catch (e) {
// if (mounted) {
// pop building dialog
Navigator.of(context).pop();
if (mounted) {
// pop building dialog
Navigator.of(context).pop();
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Transaction failed",
message: e.toString(),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
await showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Transaction failed",
message: e.toString(),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
),
onPressed: () {
Navigator.of(context).pop();
},
),
onPressed: () {
Navigator.of(context).pop();
},
),
);
},
);
// }
);
},
);
}
}
}
@ -420,7 +422,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
final wallet = ref.watch(pWallets).getWallet(walletId);
final locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
localeServiceChangeNotifierProvider.select((value) => value.locale),
);
final coin = ref.watch(pWalletCoin(walletId));
@ -483,9 +486,11 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
style: STextStyles.itemSubtitle(context),
),
Text(
ref.watch(pAmountFormatter(coin)).format(ref
.watch(pWalletBalanceTertiary(walletId))
.spendable),
ref.watch(pAmountFormatter(coin)).format(
ref
.watch(pWalletBalanceTertiary(walletId))
.spendable,
),
style: STextStyles.itemSubtitle(context),
),
],
@ -637,7 +642,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
if (!isFiro)
Text(
ref.watch(pAmountFormatter(coin)).format(
ref.watch(pWalletBalance(walletId)).spendable),
ref.watch(pWalletBalance(walletId)).spendable,
),
style: STextStyles.itemSubtitle(context),
),
],

View file

@ -349,7 +349,7 @@ class _MonkeyViewState extends ConsumerState<MonkeyView> {
),
]),
context: context,
isDesktop: Util.isDesktop,
rootNavigator: Util.isDesktop,
message: "Saving MonKey svg",
onException: (e) {
didError = true;
@ -402,7 +402,7 @@ class _MonkeyViewState extends ConsumerState<MonkeyView> {
const Duration(seconds: 2)),
]),
context: context,
isDesktop: Util.isDesktop,
rootNavigator: Util.isDesktop,
message: "Downloading MonKey png",
onException: (e) {
didError = true;
@ -500,7 +500,7 @@ class _MonkeyViewState extends ConsumerState<MonkeyView> {
Future<void>.delayed(const Duration(seconds: 2)),
]),
context: context,
isDesktop: Util.isDesktop,
rootNavigator: Util.isDesktop,
message: "Fetching MonKey",
subMessage: "We are fetching your MonKey",
onException: (e) {

View file

@ -321,7 +321,7 @@ class _OrdinalImageGroup extends ConsumerWidget {
final filePath = await showLoading<String>(
whileFuture: _savePngToFile(ref),
context: context,
isDesktop: true,
rootNavigator: true,
message: "Saving ordinal image",
onException: (e) {
didError = true;

View file

@ -10,7 +10,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/themes/theme_providers.dart';
@ -21,6 +20,7 @@ import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/date_picker/date_picker.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
@ -69,8 +69,8 @@ final ordinalFilterProvider = StateProvider<OrdinalFilter?>((_) => null);
class OrdinalsFilterView extends ConsumerStatefulWidget {
const OrdinalsFilterView({
Key? key,
}) : super(key: key);
super.key,
});
static const String routeName = "/ordinalsFilterView";
@ -146,56 +146,6 @@ class _OrdinalsFilterViewState extends ConsumerState<OrdinalsFilterView> {
DateTime? _selectedFromDate = DateTime(2007);
DateTime? _selectedToDate = DateTime.now();
MaterialRoundedDatePickerStyle _buildDatePickerStyle() {
return MaterialRoundedDatePickerStyle(
backgroundPicker: Theme.of(context).extension<StackColors>()!.popupBG,
// backgroundHeader: Theme.of(context).extension<StackColors>()!.textSubtitle2,
paddingMonthHeader: const EdgeInsets.only(top: 11),
colorArrowNext: Theme.of(context).extension<StackColors>()!.textSubtitle1,
colorArrowPrevious:
Theme.of(context).extension<StackColors>()!.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<StackColors>()!.textSubtitle3,
),
textStyleDayOnCalendarSelected:
STextStyles.datePicker400(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textWhite,
),
textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
textStyleYearButton: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textWhite,
),
// textStyleButtonAction: GoogleFonts.inter(),
);
}
MaterialRoundedYearPickerStyle _buildYearPickerStyle() {
return MaterialRoundedYearPickerStyle(
backgroundPicker: Theme.of(context).extension<StackColors>()!.popupBG,
textStyleYear: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle2,
fontSize: 16,
),
textStyleYearSelected: STextStyles.datePicker600(context).copyWith(
fontSize: 18,
),
);
}
Widget _buildDateRangePicker() {
const middleSeparatorPadding = 2.0;
const middleSeparatorWidth = 12.0;
@ -216,9 +166,6 @@ class _OrdinalsFilterViewState extends ConsumerState<OrdinalsFilterView> {
child: GestureDetector(
key: const Key("OrdinalsViewFromDatePickerKey"),
onTap: () async {
final color =
Theme.of(context).extension<StackColors>()!.accentColorDark;
final height = MediaQuery.of(context).size.height;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
@ -226,28 +173,7 @@ class _OrdinalsFilterViewState extends ConsumerState<OrdinalsFilterView> {
}
if (mounted) {
final date = await showRoundedDatePicker(
// This doesn't change statusbar color...
// background: CFColors.starryNight.withOpacity(0.8),
context: context,
initialDate: DateTime.now(),
height: height * 0.5,
theme: ThemeData(
primarySwatch: Util.createMaterialColor(
color,
),
),
//TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched
firstDate: DateTime(2007),
lastDate: DateTime.now(),
borderRadius: Constants.size.circularBorderRadius * 2,
textPositiveButton: "SELECT",
styleDatePicker: _buildDatePickerStyle(),
styleYearPicker: _buildYearPickerStyle(),
);
final date = await showSWDatePicker(context);
if (date != null) {
_selectedFromDate = date;
@ -330,9 +256,6 @@ class _OrdinalsFilterViewState extends ConsumerState<OrdinalsFilterView> {
child: GestureDetector(
key: const Key("OrdinalsViewToDatePickerKey"),
onTap: () async {
final color =
Theme.of(context).extension<StackColors>()!.accentColorDark;
final height = MediaQuery.of(context).size.height;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
@ -340,28 +263,7 @@ class _OrdinalsFilterViewState extends ConsumerState<OrdinalsFilterView> {
}
if (mounted) {
final date = await showRoundedDatePicker(
// This doesn't change statusbar color...
// background: CFColors.starryNight.withOpacity(0.8),
context: context,
height: height * 0.5,
theme: ThemeData(
primarySwatch: Util.createMaterialColor(
color,
),
),
//TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched
initialDate: DateTime.now(),
firstDate: DateTime(2007),
lastDate: DateTime.now(),
borderRadius: Constants.size.circularBorderRadius * 2,
textPositiveButton: "SELECT",
styleDatePicker: _buildDatePickerStyle(),
styleYearPicker: _buildYearPickerStyle(),
);
final date = await showSWDatePicker(context);
if (date != null) {
_selectedToDate = date;
@ -467,7 +369,7 @@ class _OrdinalsFilterViewState extends ConsumerState<OrdinalsFilterView> {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
if (context.mounted) {
Navigator.of(context).pop();
}
},
@ -840,7 +742,7 @@ class _OrdinalsFilterViewState extends ConsumerState<OrdinalsFilterView> {
);
}
}
if (mounted) {
if (context.mounted) {
Navigator.of(context).pop();
}
},

View file

@ -8,8 +8,12 @@
*
*/
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/prefs.dart';
class PayNymBot extends StatelessWidget {
const PayNymBot({
@ -28,16 +32,37 @@ class PayNymBot extends StatelessWidget {
child: SizedBox(
width: size,
height: size,
child: Image.network(
"https://paynym.is/$paymentCodeString/avatar",
loadingBuilder: (context, child, loadingProgress) =>
loadingProgress == null
? child
: const Center(
child: LoadingIndicator(),
),
child: FutureBuilder<Uint8List>(
future: _fetchImage(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(snapshot.data!);
} else if (snapshot.hasError) {
return const Center(child: Icon(Icons.error));
} else {
return const Center(); // TODO [prio=low]: Make better loading indicator.
}
},
),
),
);
}
Future<Uint8List> _fetchImage() async {
final HTTP client = HTTP();
final Uri uri = Uri.parse("https://paynym.is/$paymentCodeString/avatar");
final response = await client.get(
url: uri,
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);
if (response.code == 200) {
return Uint8List.fromList(response.bodyBytes);
} else {
throw Exception('Failed to load image');
}
}
}

View file

@ -10,31 +10,46 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/receive_view/addresses/address_tag.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_edit_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class AddressCard extends ConsumerStatefulWidget {
const AddressCard({
Key? key,
super.key,
required this.addressId,
required this.walletId,
required this.coin,
this.onPressed,
this.clipboard = const ClipboardWrapper(),
}) : super(key: key);
});
final int addressId;
final String walletId;
@ -47,6 +62,7 @@ class AddressCard extends ConsumerStatefulWidget {
}
class _AddressCardState extends ConsumerState<AddressCard> {
final _qrKey = GlobalKey();
final isDesktop = Util.isDesktop;
late Stream<AddressLabel?> stream;
@ -54,6 +70,72 @@ class _AddressCardState extends ConsumerState<AddressCard> {
AddressLabel? label;
Future<void> _capturePng(bool shouldSaveInsteadOfShare) async {
try {
final RenderRepaintBoundary boundary =
_qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary;
final ui.Image image = await boundary.toImage();
final ByteData? byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
final Uint8List pngBytes = byteData!.buffer.asUint8List();
if (shouldSaveInsteadOfShare) {
if (Util.isDesktop) {
final dir = Directory("${Platform.environment['HOME']}");
if (!dir.existsSync()) {
throw Exception(
"Home dir not found while trying to open filepicker on QR image save");
}
final path = await FilePicker.platform.saveFile(
fileName: "qrcode.png",
initialDirectory: dir.path,
);
if (path != null) {
final file = File(path);
if (file.existsSync()) {
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "$path already exists!",
context: context,
),
);
}
} else {
await file.writeAsBytes(pngBytes);
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "$path saved!",
context: context,
),
);
}
}
}
} else {
// await DocumentFileSavePlus.saveFile(
// pngBytes,
// "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png",
// "image/png");
}
} else {
final tempDir = await getTemporaryDirectory();
final file = await File("${tempDir.path}/qrcode.png").create();
await file.writeAsBytes(pngBytes);
await Share.shareFiles(["${tempDir.path}/qrcode.png"],
text: "Receive URI QR Code");
}
} catch (e) {
//todo: comeback to this
debugPrint(e.toString());
}
}
@override
void initState() {
address = MainDB.instance.isar.addresses
@ -117,16 +199,32 @@ class _AddressCardState extends ConsumerState<AddressCard> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (label!.value.isNotEmpty)
Text(
label!.value,
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.left,
),
if (label!.value.isNotEmpty)
SizedBox(
height: isDesktop ? 2 : 8,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label!.value.isNotEmpty ? label!.value : "No label",
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.left,
),
SimpleEditButton(
editValue: label!.value,
editLabel: 'label',
overrideTitle: 'Edit label',
disableIcon: true,
onValueChanged: (value) {
MainDB.instance.putAddressLabel(
label!.copyWith(
label: value,
),
);
},
),
],
),
SizedBox(
height: isDesktop ? 2 : 8,
),
Row(
children: [
Expanded(
@ -140,18 +238,152 @@ class _AddressCardState extends ConsumerState<AddressCard> {
const SizedBox(
height: 10,
),
if (label!.tags != null && label!.tags!.isNotEmpty)
Wrap(
spacing: 10,
runSpacing: 10,
children: label!.tags!
.map(
(e) => AddressTag(
tag: e,
Row(
children: [
CustomTextButton(
text: "Copy address",
onTap: () {
widget.clipboard
.setData(
ClipboardData(
text: address.value,
),
)
.toList(),
),
.then((value) {
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
),
);
}
});
},
),
const SizedBox(
width: 16,
),
CustomTextButton(
text: "Show QR code",
onTap: () async {
await showDialog<void>(
context: context,
builder: (_) {
return StackDialogBase(
child: Column(
children: [
if (label!.value.isNotEmpty)
Text(
label!.value,
style: STextStyles.w600_18(context),
),
if (label!.value.isNotEmpty)
const SizedBox(
height: 8,
),
Text(
address.value,
style:
STextStyles.w500_16(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
const SizedBox(
height: 16,
),
Center(
child: RepaintBoundary(
key: _qrKey,
child: QrImageView(
data: AddressUtils.buildUriString(
widget.coin,
address.value,
{},
),
size: 220,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.popupBG,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
const SizedBox(
height: 16,
),
Row(
children: [
if (!isDesktop)
Expanded(
child: SecondaryButton(
label: "Share",
buttonHeight: isDesktop
? ButtonHeight.l
: null,
icon: SvgPicture.asset(
Assets.svg.share,
width: 14,
height: 14,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
onPressed: () async {
await _capturePng(false);
},
),
),
if (isDesktop)
Expanded(
child: PrimaryButton(
buttonHeight: isDesktop
? ButtonHeight.l
: null,
onPressed: () async {
// TODO: add save functionality instead of share
// save works on linux at the moment
await _capturePng(true);
},
label: "Save",
icon: SvgPicture.asset(
Assets.svg.arrowDown,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
),
),
],
)
],
),
);
},
);
},
),
],
),
// if (label!.tags != null && label!.tags!.isNotEmpty)
// Wrap(
// spacing: 10,
// runSpacing: 10,
// children: label!.tags!
// .map(
// (e) => AddressTag(
// tag: e,
// ),
// )
// .toList(),
// ),
],
),
);

View file

@ -10,14 +10,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/receive_view/addresses/address_card.dart';
import 'package:stackwallet/pages/receive_view/addresses/address_details_view.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
@ -25,13 +23,8 @@ import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:tuple/tuple.dart';
import '../../../utilities/assets.dart';
import '../../../widgets/icon_widgets/x_icon.dart';
import '../../../widgets/textfield_icon_button.dart';
class WalletAddressesView extends ConsumerStatefulWidget {
const WalletAddressesView({
Key? key,
@ -50,10 +43,10 @@ class WalletAddressesView extends ConsumerStatefulWidget {
class _WalletAddressesViewState extends ConsumerState<WalletAddressesView> {
final bool isDesktop = Util.isDesktop;
String _searchString = "";
final String _searchString = "";
late final TextEditingController _searchController;
final searchFieldFocusNode = FocusNode();
// late final TextEditingController _searchController;
// final searchFieldFocusNode = FocusNode();
Future<List<int>> _search(String term) async {
if (term.isEmpty) {
@ -119,19 +112,19 @@ class _WalletAddressesViewState extends ConsumerState<WalletAddressesView> {
.findAll();
}
@override
void initState() {
_searchController = TextEditingController();
super.initState();
}
@override
void dispose() {
_searchController.dispose();
searchFieldFocusNode.dispose();
super.dispose();
}
// @override
// void initState() {
// _searchController = TextEditingController();
//
// super.initState();
// }
//
// @override
// void dispose() {
// _searchController.dispose();
// searchFieldFocusNode.dispose();
// super.dispose();
// }
@override
Widget build(BuildContext context) {
@ -165,74 +158,74 @@ class _WalletAddressesViewState extends ConsumerState<WalletAddressesView> {
),
child: Column(
children: [
SizedBox(
width: isDesktop ? 490 : null,
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: !isDesktop,
enableSuggestions: !isDesktop,
controller: _searchController,
focusNode: searchFieldFocusNode,
onChanged: (value) {
setState(() {
_searchString = value;
});
},
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
height: 1.8,
)
: STextStyles.field(context),
decoration: standardInputDecoration(
"Search...",
searchFieldFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
prefixIcon: Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 12 : 10,
vertical: isDesktop ? 18 : 16,
),
child: SvgPicture.asset(
Assets.svg.search,
width: isDesktop ? 20 : 16,
height: isDesktop ? 20 : 16,
),
),
suffixIcon: _searchController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_searchController.text = "";
_searchString = "";
});
},
),
],
),
),
)
: null,
),
),
),
),
SizedBox(
height: isDesktop ? 20 : 16,
),
// SizedBox(
// width: isDesktop ? 490 : null,
// child: ClipRRect(
// borderRadius: BorderRadius.circular(
// Constants.size.circularBorderRadius,
// ),
// child: TextField(
// autocorrect: !isDesktop,
// enableSuggestions: !isDesktop,
// controller: _searchController,
// focusNode: searchFieldFocusNode,
// onChanged: (value) {
// setState(() {
// _searchString = value;
// });
// },
// style: isDesktop
// ? STextStyles.desktopTextExtraSmall(context).copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .textFieldActiveText,
// height: 1.8,
// )
// : STextStyles.field(context),
// decoration: standardInputDecoration(
// "Search...",
// searchFieldFocusNode,
// context,
// desktopMed: isDesktop,
// ).copyWith(
// prefixIcon: Padding(
// padding: EdgeInsets.symmetric(
// horizontal: isDesktop ? 12 : 10,
// vertical: isDesktop ? 18 : 16,
// ),
// child: SvgPicture.asset(
// Assets.svg.search,
// width: isDesktop ? 20 : 16,
// height: isDesktop ? 20 : 16,
// ),
// ),
// suffixIcon: _searchController.text.isNotEmpty
// ? Padding(
// padding: const EdgeInsets.only(right: 0),
// child: UnconstrainedBox(
// child: Row(
// children: [
// TextFieldIconButton(
// child: const XIcon(),
// onTap: () async {
// setState(() {
// _searchController.text = "";
// _searchString = "";
// });
// },
// ),
// ],
// ),
// ),
// )
// : null,
// ),
// ),
// ),
// ),
// SizedBox(
// height: isDesktop ? 20 : 16,
// ),
Expanded(
child: FutureBuilder(
future: _search(_searchString),
@ -249,15 +242,17 @@ class _WalletAddressesViewState extends ConsumerState<WalletAddressesView> {
walletId: widget.walletId,
addressId: snapshot.data![index],
coin: coin,
onPressed: () {
Navigator.of(context).pushNamed(
AddressDetailsView.routeName,
arguments: Tuple2(
snapshot.data![index],
widget.walletId,
),
);
},
onPressed: !isDesktop
? null
: () {
Navigator.of(context).pushNamed(
AddressDetailsView.routeName,
arguments: Tuple2(
snapshot.data![index],
widget.walletId,
),
);
},
),
);
} else {

View file

@ -30,8 +30,12 @@ import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import 'package:stackwallet/widgets/background.dart';
@ -39,15 +43,17 @@ import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class ReceiveView extends ConsumerStatefulWidget {
const ReceiveView({
Key? key,
super.key,
required this.walletId,
this.tokenContract,
this.clipboard = const ClipboardWrapper(),
}) : super(key: key);
});
static const String routeName = "/receiveView";
@ -63,11 +69,14 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
late final Coin coin;
late final String walletId;
late final ClipboardInterface clipboard;
late final bool supportsSpark;
late final bool _supportsSpark;
late final bool _showMultiType;
String? _sparkAddress;
String? _qrcodeContent;
bool _showSparkAddress = true;
int _currentIndex = 0;
final List<AddressType> _walletAddressTypes = [];
final Map<AddressType, String> _addressMap = {};
final Map<AddressType, StreamSubscription<Address?>> _addressSubMap = {};
Future<void> generateNewAddress() async {
final wallet = ref.read(pWallets).getWallet(walletId);
@ -95,13 +104,32 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
),
);
await wallet.generateNewReceivingAddress();
final Address? address;
if (wallet is Bip39HDWallet && wallet is! BCashInterface) {
final type = DerivePathType.values.firstWhere(
(e) => e.getAddressType() == _walletAddressTypes[_currentIndex],
);
address = await wallet.generateNextReceivingAddress(
derivePathType: type,
);
await ref.read(mainDBProvider).isar.writeTxn(() async {
await ref.read(mainDBProvider).isar.addresses.put(address!);
});
} else {
await wallet.generateNewReceivingAddress();
address = null;
}
shouldPop = true;
if (mounted) {
Navigator.of(context)
.popUntil(ModalRoute.withName(ReceiveView.routeName));
setState(() {
_addressMap[_walletAddressTypes[_currentIndex]] =
address?.value ?? ref.read(pWalletReceivingAddress(walletId));
});
}
}
}
@ -140,45 +168,68 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
if (_sparkAddress != address.value) {
setState(() {
_sparkAddress = address.value;
});
}
setState(() {
_addressMap[AddressType.spark] = address.value;
});
}
}
}
StreamSubscription<Address?>? _streamSub;
@override
void initState() {
walletId = widget.walletId;
coin = ref.read(pWalletCoin(walletId));
clipboard = widget.clipboard;
supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface;
final wallet = ref.read(pWallets).getWallet(walletId);
_supportsSpark = wallet is SparkInterface;
_showMultiType = _supportsSpark ||
(wallet is! BCashInterface &&
wallet is Bip39HDWallet &&
wallet.supportedAddressTypes.length > 1);
if (supportsSpark) {
_streamSub = ref
.read(mainDBProvider)
.isar
.addresses
.where()
.walletIdEqualTo(walletId)
.filter()
.typeEqualTo(AddressType.spark)
.sortByDerivationIndexDesc()
.findFirst()
.asStream()
.listen((event) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_sparkAddress = event?.value;
});
}
_walletAddressTypes.add(coin.primaryAddressType);
if (_showMultiType) {
if (_supportsSpark) {
_walletAddressTypes.insert(0, AddressType.spark);
} else {
_walletAddressTypes.addAll((wallet as Bip39HDWallet)
.supportedAddressTypes
.where((e) => e != coin.primaryAddressType));
}
}
if (_walletAddressTypes.length > 1 && wallet is BitcoinWallet) {
_walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh);
}
_addressMap[_walletAddressTypes[_currentIndex]] =
ref.read(pWalletReceivingAddress(walletId));
if (_showMultiType) {
for (final type in _walletAddressTypes) {
_addressSubMap[type] = ref
.read(mainDBProvider)
.isar
.addresses
.where()
.walletIdEqualTo(walletId)
.filter()
.typeEqualTo(type)
.sortByDerivationIndexDesc()
.findFirst()
.asStream()
.listen((event) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_addressMap[type] =
event?.value ?? _addressMap[type] ?? "[No address yet]";
});
}
});
});
});
}
}
super.initState();
@ -186,7 +237,9 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
@override
void dispose() {
_streamSub?.cancel();
for (final subscription in _addressSubMap.values) {
subscription.cancel();
}
super.dispose();
}
@ -196,14 +249,11 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
final ticker = widget.tokenContract?.symbol ?? coin.ticker;
if (supportsSpark) {
if (_showSparkAddress) {
_qrcodeContent = _sparkAddress;
} else {
_qrcodeContent = ref.watch(pWalletReceivingAddress(walletId));
}
final String address;
if (_showMultiType) {
address = _addressMap[_walletAddressTypes[_currentIndex]]!;
} else {
_qrcodeContent = ref.watch(pWalletReceivingAddress(walletId));
address = ref.watch(pWalletReceivingAddress(walletId));
}
return Background(
@ -319,33 +369,44 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ConditionalParent(
condition: supportsSpark,
condition: _showMultiType,
builder: (child) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Address type",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.infoItemLabel,
),
),
const SizedBox(
height: 10,
),
DropdownButtonHideUnderline(
child: DropdownButton2<bool>(
value: _showSparkAddress,
child: DropdownButton2<int>(
value: _currentIndex,
items: [
DropdownMenuItem(
value: true,
child: Text(
"Spark address",
style: STextStyles.desktopTextMedium(context),
for (int i = 0;
i < _walletAddressTypes.length;
i++)
DropdownMenuItem(
value: i,
child: Text(
_supportsSpark &&
_walletAddressTypes[i] ==
AddressType.p2pkh
? "Transparent address"
: "${_walletAddressTypes[i].readableName} address",
style: STextStyles.w500_14(context),
),
),
),
DropdownMenuItem(
value: false,
child: Text(
"Transparent address",
style: STextStyles.desktopTextMedium(context),
),
),
],
onChanged: (value) {
if (value is bool && value != _showSparkAddress) {
if (value != null && value != _currentIndex) {
setState(() {
_showSparkAddress = value;
_currentIndex = value;
});
}
},
@ -363,6 +424,16 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
),
),
),
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
@ -386,89 +457,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
const SizedBox(
height: 12,
),
if (_showSparkAddress)
GestureDetector(
onTap: () {
clipboard.setData(
ClipboardData(text: _sparkAddress ?? "Error"),
);
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar,
width: 1,
),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: RoundedWhiteContainer(
child: Column(
children: [
Row(
children: [
Text(
"Your ${coin.ticker} SPARK address",
style:
STextStyles.itemSubtitle(context),
),
const Spacer(),
Row(
children: [
SvgPicture.asset(
Assets.svg.copy,
width: 15,
height: 15,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
const SizedBox(
width: 4,
),
Text(
"Copy",
style: STextStyles.link2(context),
),
],
),
],
),
const SizedBox(
height: 8,
),
Row(
children: [
Expanded(
child: Text(
_sparkAddress ?? "Error",
style: STextStyles
.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
],
),
],
),
),
),
),
if (!_showSparkAddress) child,
child,
],
),
child: GestureDetector(
@ -476,8 +465,8 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
HapticFeedback.lightImpact();
clipboard.setData(
ClipboardData(
text:
ref.watch(pWalletReceivingAddress(walletId))),
text: address,
),
);
showFloatingFlushBar(
type: FlushBarType.info,
@ -524,8 +513,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
children: [
Expanded(
child: Text(
ref.watch(
pWalletReceivingAddress(walletId)),
address,
style: STextStyles.itemSubtitle12(context),
),
),
@ -536,31 +524,44 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
),
),
),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Copy address",
onPressed: () {
HapticFeedback.lightImpact();
clipboard.setData(
ClipboardData(
text: address,
),
);
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
);
},
),
if (ref.watch(pWallets
.select((value) => value.getWallet(walletId)))
is MultiAddressInterface ||
supportsSpark)
_supportsSpark)
const SizedBox(
height: 12,
),
if (ref.watch(pWallets
.select((value) => value.getWallet(walletId)))
is MultiAddressInterface ||
supportsSpark)
TextButton(
onPressed: supportsSpark && _showSparkAddress
_supportsSpark)
SecondaryButton(
label: "Generate new address",
onPressed: _supportsSpark &&
_walletAddressTypes[_currentIndex] ==
AddressType.spark
? generateNewSparkAddress
: generateNewAddress,
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Generate new address",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
const SizedBox(
height: 30,
@ -574,7 +575,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
QrImageView(
data: AddressUtils.buildUriString(
coin,
_qrcodeContent ?? "",
address,
{},
),
size: MediaQuery.of(context).size.width / 2,
@ -585,7 +586,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
height: 20,
),
CustomTextButton(
text: "Create new QR code",
text: "Advanced options",
onTap: () async {
unawaited(Navigator.of(context).push(
RouteGenerator.getRoute(
@ -593,7 +594,7 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
RouteGenerator.useMaterialPageRoute,
builder: (_) => GenerateUriQrCodeView(
coin: coin,
receivingAddress: _qrcodeContent ?? "",
receivingAddress: address,
),
settings: const RouteSettings(
name: GenerateUriQrCodeView.routeName,

View file

@ -0,0 +1,572 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/recipient.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/fee_slider.dart';
import 'package:stackwallet/widgets/frost_scaffold.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:tuple/tuple.dart';
class FrostSendView extends ConsumerStatefulWidget {
const FrostSendView({
super.key,
required this.walletId,
required this.coin,
});
static const String routeName = "/frostSendView";
final String walletId;
final Coin coin;
@override
ConsumerState<FrostSendView> createState() => _FrostSendViewState();
}
class _FrostSendViewState extends ConsumerState<FrostSendView> {
final List<int> recipientWidgetIndexes = [0];
int _greatestWidgetIndex = 0;
late final String walletId;
late final Coin coin;
late TextEditingController noteController;
late TextEditingController onChainNoteController;
final _noteFocusNode = FocusNode();
Set<UTXO> selectedUTXOs = {};
bool _createSignLock = false;
Future<TxData> _loadingFuture() async {
final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet;
final recipients = recipientWidgetIndexes
.map((i) => ref.read(pRecipient(i).state).state)
.map((e) => (address: e!.address, amount: e!.amount!, isChange: false))
.toList(growable: false);
final txData = await wallet.frostCreateSignConfig(
txData: TxData(recipients: recipients),
changeAddress: (await wallet.getCurrentReceivingAddress())!.value,
feePerWeight: customFeeRate,
);
return txData;
}
Future<void> _createSignConfig() async {
if (_createSignLock) {
return;
}
_createSignLock = true;
try {
// wait for keyboard to disappear
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
TxData? txData;
if (mounted) {
txData = await showLoading<TxData>(
whileFuture: _loadingFuture(),
context: context,
message: "Generating sign config",
rootNavigator: Util.isDesktop,
onException: (e) {
throw e;
},
);
}
final wallet =
ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet;
if (mounted && txData != null) {
ref.read(pFrostTxData.notifier).state = txData;
ref.read(pFrostScaffoldArgs.state).state = (
info: (
walletName: wallet.info.name,
frostCurrency: wallet.cryptoCurrency,
),
walletId: walletId,
stepRoutes: FrostRouteGenerator.sendFrostTxStepRoutes,
parentNav: Navigator.of(context),
frostInterruptionDialogType:
FrostInterruptionDialogType.transactionCreation,
callerRouteName: FrostSendView.routeName,
);
await Navigator.of(context).pushNamed(
FrostStepScaffold.routeName,
);
}
} catch (e) {
if (mounted) {
unawaited(
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (context) {
return StackDialog(
title: "Create sign config failed",
message: e.toString(),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
onPressed: () {
Navigator.of(context).pop();
},
),
);
},
),
);
}
} finally {
_createSignLock = false;
}
}
int customFeeRate = 1;
bool _buttonEnabled = false;
bool _validateRecipientFormStatesHelper() {
for (final i in recipientWidgetIndexes) {
final state = ref.read(pRecipient(i));
if (state?.amount == null ||
state?.address == null ||
state!.address.isEmpty) {
return false;
}
}
return true;
}
void _validateRecipientFormStates() {
setState(() {
_buttonEnabled = _validateRecipientFormStatesHelper();
});
}
@override
void initState() {
coin = widget.coin;
walletId = widget.walletId;
noteController = TextEditingController();
super.initState();
}
@override
void dispose() {
noteController.dispose();
_noteFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final wallet = ref.watch(pWallets).getWallet(walletId);
final showCoinControl = wallet is CoinControlInterface &&
ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableCoinControl,
),
);
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 50));
}
if (context.mounted) {
Navigator.of(context).pop();
}
},
),
title: Text(
"Send ${coin.ticker}",
style: STextStyles.navBarTitle(context),
),
),
body: LayoutBuilder(
builder: (builderContext, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
// subtract top and bottom padding set in parent
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: child,
),
),
),
);
},
),
),
),
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 14,
),
child: child,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!Util.isDesktop)
Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
SvgPicture.file(
File(
ref.watch(
coinIconProvider(coin),
),
),
width: 22,
height: 22,
),
const SizedBox(
width: 6,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ref.watch(pWalletName(walletId)),
style: STextStyles.titleBold12(context)
.copyWith(fontSize: 14),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
// const SizedBox(
// height: 2,
// ),
Text(
"Available balance",
style: STextStyles.label(context)
.copyWith(fontSize: 10),
),
],
),
Util.isDesktop
? const SizedBox(
height: 24,
)
: const Spacer(),
GestureDetector(
onTap: () {},
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
ref.watch(pAmountFormatter(coin)).format(ref
.watch(pWalletBalance(walletId))
.spendable),
style:
STextStyles.titleBold12(context).copyWith(
fontSize: 10,
),
textAlign: TextAlign.right,
),
],
),
),
)
],
),
),
),
SizedBox(
height: recipientWidgetIndexes.length > 1 ? 8 : 16,
),
Column(
children: [
for (int i = 0; i < recipientWidgetIndexes.length; i++)
ConditionalParent(
condition: recipientWidgetIndexes.length > 1,
builder: (child) => Padding(
padding: const EdgeInsets.only(top: 8),
child: child,
),
child: Recipient(
key: Key(
"recipientKey_${recipientWidgetIndexes[i]}",
),
index: recipientWidgetIndexes[i],
displayNumber: i + 1,
coin: coin,
onChanged: () {
_validateRecipientFormStates();
},
remove: i == 0 && recipientWidgetIndexes.length == 1
? null
: () {
ref
.read(pRecipient(recipientWidgetIndexes[i])
.notifier)
.state = null;
recipientWidgetIndexes.removeAt(i);
setState(() {});
_validateRecipientFormStates();
},
addAnotherRecipientTapped: () {
// used for tracking recipient forms
_greatestWidgetIndex++;
recipientWidgetIndexes.add(_greatestWidgetIndex);
setState(() {});
_validateRecipientFormStates();
},
sendAllTapped: () {
return ref.read(pAmountFormatter(coin)).format(
ref.read(pWalletBalance(walletId)).spendable,
withUnitName: false,
);
},
),
),
],
),
if (recipientWidgetIndexes.length > 1)
const SizedBox(
height: 12,
),
if (recipientWidgetIndexes.length > 1)
SecondaryButton(
width: double.infinity,
label: "Add recipient",
onPressed: () {
// used for tracking recipient forms
_greatestWidgetIndex++;
recipientWidgetIndexes.add(_greatestWidgetIndex);
setState(() {});
},
),
if (showCoinControl)
const SizedBox(
height: 8,
),
if (showCoinControl)
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Coin control",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
CustomTextButton(
text: selectedUTXOs.isEmpty
? "Select coins"
: "Selected coins (${selectedUTXOs.length})",
onTap: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
}
if (mounted) {
// TODO: [prio=high] make sure this coincontrol works correctly
Amount? amount;
final result = await Navigator.of(context).pushNamed(
CoinControlView.routeName,
arguments: Tuple4(
walletId,
CoinControlViewType.use,
amount,
selectedUTXOs,
),
);
if (result is Set<UTXO>) {
setState(() {
selectedUTXOs = result;
});
}
}
},
),
],
),
),
const SizedBox(
height: 12,
),
Text(
"Note (optional)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: noteController,
focusNode: _noteFocusNode,
style: STextStyles.field(context),
onChanged: (_) => setState(() {}),
decoration: standardInputDecoration(
"Type something...",
_noteFocusNode,
context,
).copyWith(
suffixIcon: noteController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
noteController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
const SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.only(
bottom: 12,
top: 16,
),
child: FeeSlider(
coin: coin,
showWU: true,
onSatVByteChanged: (rate) {
customFeeRate = rate;
},
),
),
Util.isDesktop
? const SizedBox(
height: 12,
)
: const Spacer(),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Create multisig transaction",
enabled: _buttonEnabled,
onPressed: _createSignConfig,
),
const SizedBox(
height: 16,
),
],
),
),
);
}
}

View file

@ -0,0 +1,447 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/global/locale_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
import 'package:stackwallet/utilities/amount/amount_unit.dart';
import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
//TODO: move the following two providers elsewhere
final pClipboard =
Provider<ClipboardInterface>((ref) => const ClipboardWrapper());
final pBarcodeScanner =
Provider<BarcodeScannerInterface>((ref) => const BarcodeScannerWrapper());
// final _pPrice = Provider.family<Decimal, Coin>((ref, coin) {
// return ref.watch(
// priceAnd24hChangeNotifierProvider
// .select((value) => value.getPrice(coin).item1),
// );
// });
final pRecipient =
StateProvider.family<({String address, Amount? amount})?, int>(
(ref, index) => null);
class Recipient extends ConsumerStatefulWidget {
const Recipient({
super.key,
required this.index,
required this.displayNumber,
required this.coin,
this.remove,
this.onChanged,
required this.addAnotherRecipientTapped,
required this.sendAllTapped,
});
final int index;
final int displayNumber;
final Coin coin;
final VoidCallback? remove;
final VoidCallback? onChanged;
final VoidCallback addAnotherRecipientTapped;
final String Function() sendAllTapped;
@override
ConsumerState<Recipient> createState() => _RecipientState();
}
class _RecipientState extends ConsumerState<Recipient> {
late final TextEditingController addressController, amountController;
late final FocusNode addressFocusNode, amountFocusNode;
bool _addressIsEmpty = true;
final bool _cryptoAmountChangeLock = false;
bool get isSingle => widget.remove == null;
void _updateRecipientData() {
final address = addressController.text;
final amount =
ref.read(pAmountFormatter(widget.coin)).tryParse(amountController.text);
ref.read(pRecipient(widget.index).notifier).state = (
address: address,
amount: amount,
);
widget.onChanged?.call();
}
void _cryptoAmountChanged() async {
if (!_cryptoAmountChangeLock) {
Amount? cryptoAmount = ref.read(pAmountFormatter(widget.coin)).tryParse(
amountController.text,
);
if (cryptoAmount != null) {
if (ref.read(pRecipient(widget.index))?.amount != null &&
ref.read(pRecipient(widget.index))?.amount == cryptoAmount) {
return;
}
// final price = ref.read(_pPrice(widget.coin));
//
// if (price > Decimal.zero) {
// baseController.text = (cryptoAmount.decimal * price)
// .toAmount(
// fractionDigits: 2,
// )
// .fiatString(
// locale: ref.read(localeServiceChangeNotifierProvider).locale,
// );
// }
} else {
cryptoAmount = null;
// baseController.text = "";
}
_updateRecipientData();
}
}
@override
void initState() {
addressController = TextEditingController();
amountController = TextEditingController();
// baseController = TextEditingController();
final amount = ref.read(pRecipient(widget.index))?.amount;
if (amount != null) {
amountController.text = ref
.read(pAmountFormatter(widget.coin))
.format(amount, withUnitName: false);
}
addressController.text = ref.read(pRecipient(widget.index))?.address ?? "";
_addressIsEmpty = addressController.text.isEmpty;
addressFocusNode = FocusNode();
amountFocusNode = FocusNode();
// baseFocusNode = FocusNode();
amountController.addListener(_cryptoAmountChanged);
super.initState();
}
@override
void dispose() {
amountController.removeListener(_cryptoAmountChanged);
addressController.dispose();
amountController.dispose();
// baseController.dispose();
addressFocusNode.dispose();
amountFocusNode.dispose();
// baseFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final String locale = ref.watch(
localeServiceChangeNotifierProvider.select(
(value) => value.locale,
),
);
return RoundedContainer(
color: Colors.transparent,
padding: const EdgeInsets.all(0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isSingle ? "Send to" : "Recipient ${widget.displayNumber}",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
CustomTextButton(
text: isSingle ? "Add another recipient" : "Remove",
onTap:
isSingle ? widget.addAnotherRecipientTapped : widget.remove,
),
],
),
const SizedBox(
height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("sendViewAddressFieldKey"),
controller: addressController,
readOnly: false,
autocorrect: false,
enableSuggestions: false,
focusNode: addressFocusNode,
style: STextStyles.field(context),
onChanged: (_) {
_updateRecipientData();
setState(() {
_addressIsEmpty = addressController.text.isEmpty;
});
},
decoration: standardInputDecoration(
"Enter ${widget.coin.ticker} address",
addressFocusNode,
context,
).copyWith(
contentPadding: const EdgeInsets.only(
left: 16,
top: 6,
bottom: 8,
right: 5,
),
suffixIcon: Padding(
padding: _addressIsEmpty
? const EdgeInsets.only(right: 8)
: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
!_addressIsEmpty
? TextFieldIconButton(
semanticsLabel:
"Clear Button. Clears The Address Field Input.",
key: const Key(
"sendViewClearAddressFieldButtonKey"),
onTap: () {
addressController.text = "";
setState(() {
_addressIsEmpty = true;
});
_updateRecipientData();
},
child: const XIcon(),
)
: TextFieldIconButton(
semanticsLabel:
"Paste Button. Pastes From Clipboard To Address Field Input.",
key: const Key(
"sendViewPasteAddressFieldButtonKey"),
onTap: () async {
final ClipboardData? data = await ref
.read(pClipboard)
.getData(Clipboard.kTextPlain);
if (data?.text != null &&
data!.text!.isNotEmpty) {
String content = data.text!.trim();
if (content.contains("\n")) {
content = content.substring(
0, content.indexOf("\n"));
}
addressController.text = content.trim();
setState(() {
_addressIsEmpty =
addressController.text.isEmpty;
});
_updateRecipientData();
}
},
child: _addressIsEmpty
? const ClipboardIcon()
: const XIcon(),
),
if (_addressIsEmpty)
TextFieldIconButton(
semanticsLabel: "Scan QR Button. "
"Opens Camera For Scanning QR Code.",
key: const Key(
"sendViewScanQrButtonKey",
),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75,
),
);
}
final qrResult =
await ref.read(pBarcodeScanner).scan();
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
/// TODO: deal with address utils
final results =
AddressUtils.parseUri(qrResult.rawContent);
Logging.instance.log(
"qrResult parsed: $results",
level: LogLevel.Info,
);
if (results.isNotEmpty &&
results["scheme"] ==
widget.coin.uriScheme) {
// auto fill address
addressController.text =
(results["address"] ?? "").trim();
// autofill amount field
if (results["amount"] != null) {
final Amount amount =
Decimal.parse(results["amount"]!)
.toAmount(
fractionDigits: widget.coin.decimals,
);
amountController.text = ref
.read(pAmountFormatter(widget.coin))
.format(
amount,
withUnitName: false,
);
}
} else {
addressController.text =
qrResult.rawContent.trim();
}
setState(() {
_addressIsEmpty =
addressController.text.isEmpty;
});
_updateRecipientData();
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while "
"trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
},
child: const QrCodeIcon(),
),
],
),
),
),
),
),
),
SizedBox(
height: isSingle ? 12 : 8,
),
if (isSingle)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
// disable send all since the frost tx creation logic isn't there (yet?)
const Spacer(),
// CustomTextButton(
// text: "Send all ${widget.coin.ticker}",
// onTap: () {
// amountController.text = widget.sendAllTapped();
// _cryptoAmountChanged();
// },
// ),
],
),
if (isSingle)
const SizedBox(
height: 8,
),
TextField(
autocorrect: false,
enableSuggestions: false,
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
key: const Key("amountInputFieldCryptoTextFieldKey"),
controller: amountController,
focusNode: amountFocusNode,
onChanged: (_) {
_updateRecipientData();
},
keyboardType: Util.isDesktop
? null
: const TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
textAlign: TextAlign.right,
inputFormatters: [
AmountInputFormatter(
decimals: widget.coin.decimals,
unit: ref.watch(pAmountUnit(widget.coin)),
locale: locale,
),
],
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
top: 12,
right: 12,
),
hintText: "0",
hintStyle: STextStyles.fieldLabel(context).copyWith(
fontSize: 14,
),
prefixIcon: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
ref
.watch(pAmountUnit(widget.coin))
.unitForCoin(widget.coin),
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class FrostSendStep1a extends ConsumerStatefulWidget {
const FrostSendStep1a({super.key});
static const String routeName = "/FrostSendStep1a";
static const String title = "FROST transaction";
@override
ConsumerState<FrostSendStep1a> createState() => _FrostSendStep1aState();
}
class _FrostSendStep1aState extends ConsumerState<FrostSendStep1a> {
static const steps2to4 = [
"Wait for them to import the transaction config.",
"Verify that everyone has filled out their forms before continuing. If you "
"try to continue before everyone is ready, the process will be "
"canceled.",
"Check the box and press “Attempt sign”.",
];
late final int _threshold;
bool _userVerifyContinue = false;
bool _attemptSignLock = false;
Future<void> _attemptSign() async {
if (_attemptSignLock) {
return;
}
_attemptSignLock = true;
try {
final wallet = ref.read(pWallets).getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
) as BitcoinFrostWallet;
final attemptSignRes = await wallet.frostAttemptSignConfig(
config: ref.read(pFrostTxData.state).state!.frostMSConfig!,
);
ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes;
ref.read(pFrostCreateCurrentStep.state).state = 2;
await Navigator.of(context).pushNamed(
ref
.read(pFrostScaffoldArgs)!
.stepRoutes[ref.read(pFrostCreateCurrentStep) - 1]
.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Error,
);
} finally {
_attemptSignLock = false;
}
}
@override
void initState() {
final wallet = ref.read(pWallets).getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
) as BitcoinFrostWallet;
_threshold = wallet.frostInfo.threshold;
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(pFrostMyName.state).state = wallet.frostInfo.myName;
});
super.initState();
}
@override
Widget build(BuildContext context) {
final double qrImageSize =
Util.isDesktop ? 360 : MediaQuery.of(context).size.width / 1.67;
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"1.",
style: STextStyles.w500_12(context),
),
const SizedBox(
width: 4,
),
Expanded(
child: RichText(
text: TextSpan(
children: [
TextSpan(
text:
"Share this config with the group members. ",
style: STextStyles.w500_12(context),
),
TextSpan(
text:
"You must have the threshold number of signatures (including yours) to send the transaction.",
style: STextStyles.w600_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.customTextButtonEnabledText,
),
),
],
),
),
),
],
),
for (int i = 0; i < steps2to4.length; i++)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${i + 2}.",
style: STextStyles.w500_12(context),
),
const SizedBox(
width: 4,
),
Expanded(
child: Text(
steps2to4[i],
style: STextStyles.w500_12(context),
),
),
],
),
),
],
),
),
SizedBox(
height: Util.isDesktop ? 20 : 12,
),
SizedBox(
height: qrImageSize,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QrImageView(
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
size: qrImageSize,
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
],
),
),
SizedBox(
height: Util.isDesktop ? 20 : 12,
),
DetailItem(
title: "Encoded transaction config",
detail: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
button: Util.isDesktop
? IconCopyButton(
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
)
: SimpleCopyButton(
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
),
),
SizedBox(
height: Util.isDesktop ? 20 : 12,
),
DetailItem(
title: "Threshold",
detail: "$_threshold signatures",
horizontal: true,
),
SizedBox(
height: Util.isDesktop ? 20 : 12,
),
if (!Util.isDesktop)
const Spacer(
flex: 2,
),
CheckboxTextButton(
label: "I have verified that everyone has imported the config and "
"is ready to sign",
onChanged: (value) {
setState(() {
_userVerifyContinue = value;
});
},
),
SizedBox(
height: Util.isDesktop ? 20 : 12,
),
PrimaryButton(
label: "Attempt sign",
enabled: _userVerifyContinue,
onPressed: () {
_attemptSign();
},
),
],
),
);
}
}

View file

@ -0,0 +1,198 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/frost_step_user_steps.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/textfields/frost_step_field.dart';
class FrostSendStep1b extends ConsumerStatefulWidget {
const FrostSendStep1b({super.key});
static const String routeName = "/FrostSendStep1b";
static const String title = "Sign FROST transaction";
@override
ConsumerState<FrostSendStep1b> createState() => _FrostSendStep1bState();
}
class _FrostSendStep1bState extends ConsumerState<FrostSendStep1b> {
static const info = [
"Scan the config QR code or paste the code provided by the member "
"initiating this transaction.",
"Wait for other members to finish entering their information.",
"Verify that everyone has filled out their forms before continuing. If you "
"try to continue before everyone is ready, the process will be "
"canceled.",
"Check the box and press “Start signing”.",
];
late final TextEditingController configFieldController;
late final FocusNode configFocusNode;
bool _configEmpty = true, _userVerifyContinue = false;
bool _attemptSignLock = false;
Future<void> _attemptSign() async {
if (_attemptSignLock) {
return;
}
_attemptSignLock = true;
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
final config = configFieldController.text;
final wallet = ref.read(pWallets).getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
) as BitcoinFrostWallet;
final data = Frost.extractDataFromSignConfig(
signConfig: config,
coin: wallet.cryptoCurrency,
);
final utxos = await ref
.read(mainDBProvider)
.getUTXOs(wallet.walletId)
.filter()
.anyOf(
data.inputs,
(q, e) => q
.txidEqualTo(Format.uint8listToString(e.hash))
.and()
.valueEqualTo(e.value)
.and()
.voutEqualTo(e.vout))
.findAll();
// TODO add more data from 'data' and display to user ?
ref.read(pFrostTxData.notifier).state = TxData(
frostMSConfig: config,
recipients: data.recipients
.map((e) => (address: e.address, amount: e.amount, isChange: false))
.toList(),
utxos: utxos.toSet(),
);
final attemptSignRes = await wallet.frostAttemptSignConfig(
config: ref.read(pFrostTxData.state).state!.frostMSConfig!,
);
ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes;
ref.read(pFrostCreateCurrentStep.state).state = 2;
await Navigator.of(context).pushNamed(
ref
.read(pFrostScaffoldArgs)!
.stepRoutes[ref.read(pFrostCreateCurrentStep) - 1]
.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Error,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Import and attempt sign config failed",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_attemptSignLock = false;
}
}
@override
void initState() {
configFieldController = TextEditingController();
configFocusNode = FocusNode();
final wallet = ref.read(pWallets).getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
) as BitcoinFrostWallet;
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(pFrostMyName.state).state = wallet.frostInfo.myName;
});
super.initState();
}
@override
void dispose() {
configFieldController.dispose();
configFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const FrostStepUserSteps(
userSteps: info,
),
const SizedBox(height: 20),
FrostStepField(
controller: configFieldController,
focusNode: configFocusNode,
showQrScanOption: true,
label: "Import sign config",
hint: "Enter config",
onChanged: (_) {
setState(() {
_configEmpty = configFieldController.text.isEmpty;
});
},
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 12,
),
CheckboxTextButton(
label: "I have verified that everyone has imported he config and"
" is ready to sign",
onChanged: (value) {
setState(() {
_userVerifyContinue = value;
});
},
),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Start signing",
enabled: !_configEmpty && _userVerifyContinue,
onPressed: () {
_attemptSign();
},
),
],
),
);
}
}

View file

@ -0,0 +1,311 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/textfields/frost_step_field.dart';
class FrostSendStep2 extends ConsumerStatefulWidget {
const FrostSendStep2({super.key});
static const String routeName = "/FrostSendStep2";
static const String title = "Preprocesses";
@override
ConsumerState<FrostSendStep2> createState() => _FrostSendStep2State();
}
class _FrostSendStep2State extends ConsumerState<FrostSendStep2> {
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final String myName;
late final List<String> participantsWithoutMe;
late final String myPreprocess;
late final int myIndex;
late final int threshold;
final List<bool> fieldIsEmptyFlags = [];
int countPreprocesses() {
// own preprocess is not included in controllers and must be set here
int count = 1;
for (final controller in controllers) {
if (controller.text.isNotEmpty) {
count++;
}
}
return count;
}
@override
void initState() {
final wallet = ref.read(pWallets).getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
) as BitcoinFrostWallet;
final frostInfo = wallet.frostInfo;
myName = frostInfo.myName;
threshold = frostInfo.threshold;
participantsWithoutMe =
List.from(frostInfo.participants); // Copy so it isn't fixed-length.
myIndex = participantsWithoutMe.indexOf(frostInfo.myName);
myPreprocess = ref.read(pFrostAttemptSignData.state).state!.preprocess;
participantsWithoutMe.removeAt(myIndex);
for (int i = 0; i < participantsWithoutMe.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"1.",
style: STextStyles.w500_12(context),
),
const SizedBox(
width: 4,
),
Expanded(
child: Text(
"Share your preprocess with other signing group members.",
style: STextStyles.w500_12(context),
),
),
],
),
const SizedBox(
height: 4,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"2.",
style: STextStyles.w500_12(context),
),
const SizedBox(
width: 4,
),
Expanded(
child: RichText(
text: TextSpan(
children: [
TextSpan(
text:
"Enter their preprocesses into the corresponding fields. ",
style: STextStyles.w500_12(context),
),
TextSpan(
text: "You must have the threshold number of "
"preprocesses (including yours) to send this transaction.",
style: STextStyles.w600_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.customTextButtonEnabledText,
),
),
],
),
),
),
],
),
],
),
),
const SizedBox(
height: 12,
),
DetailItem(
title: "Threshold",
detail: "$threshold signatures",
horizontal: true,
),
const SizedBox(
height: 12,
),
DetailItem(
title: "My name",
detail: myName,
button: Util.isDesktop
? IconCopyButton(
data: myName,
)
: SimpleCopyButton(
data: myName,
),
),
const SizedBox(
height: 12,
),
DetailItem(
title: "My preprocess",
detail: myPreprocess,
button: Util.isDesktop
? IconCopyButton(
data: myPreprocess,
)
: SimpleCopyButton(
data: myPreprocess,
),
),
const SizedBox(height: 12),
FrostQrDialogPopupButton(
data: myPreprocess,
),
const SizedBox(
height: 12,
),
RoundedWhiteContainer(
child: Text(
"You need to obtain ${threshold - 1} preprocess from signing members to send this transaction.",
style: STextStyles.label(context),
),
),
const SizedBox(
height: 12,
),
Builder(builder: (context) {
final count = countPreprocesses();
final colors = Theme.of(context).extension<StackColors>()!;
return DetailItem(
title: "Required preprocesses",
detail: "$count of $threshold",
horizontal: true,
overrideDetailTextColor: count >= threshold
? colors.accentColorGreen
: colors.accentColorRed,
);
}),
const SizedBox(
height: 12,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < participantsWithoutMe.length; i++)
FrostStepField(
label: participantsWithoutMe[i],
hint: "Enter ${participantsWithoutMe[i]}'s preprocess",
controller: controllers[i],
focusNode: focusNodes[i],
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] = controllers[i].text.isEmpty;
});
},
showQrScanOption: true,
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Generate shares",
enabled: countPreprocesses() >= threshold,
onPressed: () async {
// collect Preprocess strings (not including my own)
final preprocesses = controllers.map((e) => e.text).toList();
// collect participants who are involved in this transaction
final List<String> requiredParticipantsUnordered = [];
for (int i = 0; i < participantsWithoutMe.length; i++) {
if (preprocesses[i].isNotEmpty) {
requiredParticipantsUnordered.add(participantsWithoutMe[i]);
}
}
ref.read(pFrostSelectParticipantsUnordered.notifier).state =
requiredParticipantsUnordered;
// insert an empty string at my index
preprocesses.insert(myIndex, "");
try {
ref.read(pFrostContinueSignData.notifier).state =
Frost.continueSigning(
machinePtr:
ref.read(pFrostAttemptSignData.state).state!.machinePtr,
preprocesses: preprocesses,
);
ref.read(pFrostCreateCurrentStep.state).state = 3;
await Navigator.of(context).pushNamed(
ref
.read(pFrostScaffoldArgs)!
.stepRoutes[ref.read(pFrostCreateCurrentStep) - 1]
.routeName,
);
// await Navigator.of(context).pushNamed(
// FrostContinueSignView.routeName,
// arguments: widget.walletId,
// );
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (context.mounted) {
return await showDialog<void>(
context: context,
builder: (_) => const FrostErrorDialog(
title: "Failed to continue signing",
),
);
}
}
},
),
],
),
);
}
}

View file

@ -0,0 +1,253 @@
import 'package:coinlib_flutter/coinlib_flutter.dart' as cl;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/checkbox_text_button.dart';
import 'package:stackwallet/widgets/custom_buttons/frost_qr_dialog_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/dialogs/frost/frost_error_dialog.dart';
import 'package:stackwallet/widgets/frost_step_user_steps.dart';
import 'package:stackwallet/widgets/textfields/frost_step_field.dart';
class FrostSendStep3 extends ConsumerStatefulWidget {
const FrostSendStep3({super.key});
static const String routeName = "/FrostSendStep3";
static const String title = "Shares";
@override
ConsumerState<FrostSendStep3> createState() => _FrostSendStep3State();
}
class _FrostSendStep3State extends ConsumerState<FrostSendStep3> {
static const info = [
"Send your share to other signing group members.",
"Enter their shares into the corresponding fields.",
];
final List<TextEditingController> controllers = [];
final List<FocusNode> focusNodes = [];
late final String myName;
late final List<String> participantsWithoutMe;
late final List<String> participantsAll;
late final String myShare;
late final int myIndex;
final List<bool> fieldIsEmptyFlags = [];
bool _userVerifyContinue = false;
@override
void initState() {
final wallet = ref.read(pWallets).getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
) as BitcoinFrostWallet;
final frostInfo = wallet.frostInfo;
myName = frostInfo.myName;
participantsAll = frostInfo.participants;
myIndex = frostInfo.participants.indexOf(frostInfo.myName);
myShare = ref.read(pFrostContinueSignData.state).state!.share;
participantsWithoutMe = frostInfo.participants
.toSet()
.intersection(
ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet())
.toList();
participantsWithoutMe.remove(myName);
for (int i = 0; i < participantsWithoutMe.length; i++) {
controllers.add(TextEditingController());
focusNodes.add(FocusNode());
fieldIsEmptyFlags.add(true);
}
super.initState();
}
@override
void dispose() {
for (int i = 0; i < controllers.length; i++) {
controllers[i].dispose();
}
for (int i = 0; i < focusNodes.length; i++) {
focusNodes[i].dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const FrostStepUserSteps(
userSteps: info,
),
const SizedBox(
height: 12,
),
DetailItem(
title: "My name",
detail: myName,
button: Util.isDesktop
? IconCopyButton(
data: myName,
)
: SimpleCopyButton(
data: myName,
),
),
const SizedBox(
height: 12,
),
DetailItem(
title: "My share",
detail: myShare,
button: Util.isDesktop
? IconCopyButton(
data: myShare,
)
: SimpleCopyButton(
data: myShare,
),
),
const SizedBox(
height: 12,
),
FrostQrDialogPopupButton(
data: myShare,
),
const SizedBox(
height: 12,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < participantsWithoutMe.length; i++)
FrostStepField(
label: participantsWithoutMe[i],
hint: "Enter ${participantsWithoutMe[i]}'s share",
controller: controllers[i],
focusNode: focusNodes[i],
onChanged: (_) {
setState(() {
fieldIsEmptyFlags[i] = controllers[i].text.isEmpty;
});
},
showQrScanOption: true,
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 12,
),
CheckboxTextButton(
label: "I have verified that everyone has my share",
onChanged: (value) {
setState(() {
_userVerifyContinue = value;
});
},
),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Generate transaction",
enabled: _userVerifyContinue &&
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
onPressed: () async {
// collect Share strings
final sharesCollected = controllers.map((e) => e.text).toList();
final List<String> shares = [];
for (final participant in participantsAll) {
if (participantsWithoutMe.contains(participant)) {
shares.add(sharesCollected[
participantsWithoutMe.indexOf(participant)]);
} else {
shares.add("");
}
}
try {
final rawTx = Frost.completeSigning(
machinePtr:
ref.read(pFrostContinueSignData.state).state!.machinePtr,
shares: shares,
);
final tx = cl.Transaction.fromHex(rawTx);
final txData = ref.read(pFrostTxData)!;
final fractionDigits =
txData.recipients!.first.amount.fractionDigits;
final inputTotal = Amount(
rawValue: txData.utxos!
.map((e) => BigInt.from(e.value))
.reduce((v, e) => v += e),
fractionDigits: fractionDigits,
);
final outputTotal = Amount(
rawValue:
tx.outputs.map((e) => e.value).reduce((v, e) => v += e),
fractionDigits: fractionDigits,
);
ref.read(pFrostTxData.state).state = txData.copyWith(
raw: rawTx,
fee: inputTotal - outputTotal,
frostSigners: [
myName,
...participantsWithoutMe,
],
);
ref.read(pFrostCreateCurrentStep.state).state = 4;
await Navigator.of(context).pushNamed(
ref
.read(pFrostScaffoldArgs)!
.stepRoutes[ref.read(pFrostCreateCurrentStep) - 1]
.routeName,
);
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (context.mounted) {
return await showDialog<void>(
context: context,
builder: (_) => const FrostErrorDialog(
title: "Failed to complete signing process",
),
);
}
}
},
),
],
),
);
}
}

View file

@ -0,0 +1,296 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/expandable.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class FrostSendStep4 extends ConsumerStatefulWidget {
const FrostSendStep4({super.key});
static const String routeName = "/FrostSendStep4";
static const String title = "Preview transaction";
@override
ConsumerState<FrostSendStep4> createState() => _FrostSendStep4State();
}
class _FrostSendStep4State extends ConsumerState<FrostSendStep4> {
final List<bool> _expandedStates = [];
bool _broadcastLock = false;
late final CryptoCurrency cryptoCurrency;
@override
void initState() {
final wallet = ref.read(pWallets).getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
) as BitcoinFrostWallet;
cryptoCurrency = wallet.cryptoCurrency;
for (final _ in ref.read(pFrostTxData)!.recipients!) {
_expandedStates.add(false);
}
super.initState();
}
@override
Widget build(BuildContext context) {
final signerNames = ref.watch(pFrostTxData)!.frostSigners!;
final recipients = ref.watch(pFrostTxData)!.recipients!;
final String signers;
if (signerNames.length > 1) {
signers = signerNames
.sublist(1)
.fold(signerNames.first, (pv, e) => pv += ", $e");
} else {
signers = signerNames.first;
}
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (kDebugMode)
DetailItem(
title: "Tx hex (debug mode only)",
detail: ref.watch(pFrostTxData)!.raw!,
button: Util.isDesktop
? IconCopyButton(
data: ref.watch(pFrostTxData)!.raw!,
)
: SimpleCopyButton(
data: ref.watch(pFrostTxData)!.raw!,
),
),
if (kDebugMode)
const SizedBox(
height: 12,
),
Text(
"Send ${cryptoCurrency.coin.ticker}",
style: STextStyles.w600_20(context),
),
const SizedBox(
height: 12,
),
recipients.length == 1
? _Recipient(
address: recipients[0].address,
amount: ref
.watch(pAmountFormatter(cryptoCurrency.coin))
.format(recipients[0].amount),
)
: Column(
children: [
for (int i = 0; i < recipients.length; i++)
Padding(
padding: const EdgeInsets.only(top: 10),
child: Expandable(
onExpandChanged: (state) {
setState(() {
_expandedStates[i] =
state == ExpandableState.expanded;
});
},
header: Padding(
padding: const EdgeInsets.only(top: 12, bottom: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Recipient ${i + 1}",
style: STextStyles.itemSubtitle(context),
),
SvgPicture.asset(
_expandedStates[i]
? Assets.svg.chevronUp
: Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
],
),
),
body: _Recipient(
address: recipients[i].address,
amount: ref
.watch(pAmountFormatter(cryptoCurrency.coin))
.format(recipients[i].amount),
),
),
),
],
),
const SizedBox(
height: 12,
),
DetailItem(
title: "Transaction fee",
detail: ref
.watch(pAmountFormatter(cryptoCurrency.coin))
.format(ref.watch(pFrostTxData)!.fee!),
horizontal: true,
),
const SizedBox(
height: 12,
),
DetailItem(
title: "Total",
detail: ref.watch(pAmountFormatter(cryptoCurrency.coin)).format(
ref.watch(pFrostTxData)!.fee! +
recipients.map((e) => e.amount).reduce((v, e) => v += e)),
horizontal: true,
),
const SizedBox(
height: 12,
),
DetailItem(
title: "Note",
detail: ref.watch(pFrostTxData)!.note ?? "",
),
const SizedBox(
height: 12,
),
DetailItem(
title: "Signers",
detail: signers,
),
const SizedBox(
height: 12,
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 12,
),
PrimaryButton(
label: "Approve transaction",
onPressed: () async {
if (_broadcastLock) {
return;
}
_broadcastLock = true;
try {
Exception? ex;
final txData = await showLoading(
whileFuture: ref
.read(pWallets)
.getWallet(
ref.read(pFrostScaffoldArgs)!.walletId!,
)
.confirmSend(
txData: ref.read(pFrostTxData)!,
),
context: context,
message: "Broadcasting transaction to network",
rootNavigator: true, // used to pop using root nav
onException: (e) {
ex = e;
},
);
if (ex != null) {
throw ex!;
}
if (context.mounted) {
if (txData != null) {
ref.read(pFrostScaffoldCanPopDesktop.notifier).state = true;
ref.read(pFrostTxData.state).state = txData;
ref.read(pFrostScaffoldArgs)!.parentNav.popUntil(
ModalRoute.withName(
Util.isDesktop
? MyStackView.routeName
: WalletView.routeName,
),
);
}
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (context.mounted) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Broadcast error",
message: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
onOkPressed:
Navigator.of(context, rootNavigator: true).pop,
),
);
}
} finally {
_broadcastLock = false;
}
},
),
],
),
);
}
}
class _Recipient extends StatelessWidget {
const _Recipient({
super.key,
required this.address,
required this.amount,
});
final String address;
final String amount;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
DetailItem(
title: "Address",
detail: address,
),
const SizedBox(
height: 6,
),
DetailItem(
title: "Amount",
detail: amount,
horizontal: true,
),
],
);
}
}

View file

@ -154,8 +154,10 @@ class _SendViewState extends ConsumerState<SendView> {
// .state = true,
// );
Logging.instance.log("qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info);
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
final results = AddressUtils.parseUri(qrResult.rawContent);
@ -213,8 +215,9 @@ class _SendViewState extends ConsumerState<SendView> {
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning);
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
}
@ -280,8 +283,10 @@ class _SendViewState extends ConsumerState<SendView> {
return;
}
_cachedAmountToSend = amount;
Logging.instance.log("it changed $amount $_cachedAmountToSend",
level: LogLevel.Info);
Logging.instance.log(
"it changed $amount $_cachedAmountToSend",
level: LogLevel.Info,
);
final price =
ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1;
@ -572,9 +577,10 @@ class _SendViewState extends ConsumerState<SendView> {
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
onPressed: () {
Navigator.of(context).pop(false);
@ -616,6 +622,9 @@ class _SendViewState extends ConsumerState<SendView> {
builder: (context) {
return BuildingTransactionDialog(
coin: wallet.info.coin,
isSpark: wallet is FiroWallet &&
ref.read(publicPrivateBalanceStateProvider.state).state ==
FiroType.spark,
onCancel: () {
wasCancelled = true;
@ -645,7 +654,7 @@ class _SendViewState extends ConsumerState<SendView> {
address: widget.accountLite!.code,
amount: amount,
isChange: false,
)
),
],
satsPerVByte: isCustomFee ? customFeeRate : null,
feeRateType: feeRate,
@ -668,7 +677,7 @@ class _SendViewState extends ConsumerState<SendView> {
amount: amount,
memo: memoController.text,
isChange: false,
)
),
],
feeRateType: ref.read(feeRateTypeStateProvider),
satsPerVByte: isCustomFee ? customFeeRate : null,
@ -687,7 +696,7 @@ class _SendViewState extends ConsumerState<SendView> {
address: _address!,
amount: amount,
isChange: false,
)
),
],
feeRateType: ref.read(feeRateTypeStateProvider),
satsPerVByte: isCustomFee ? customFeeRate : null,
@ -709,7 +718,7 @@ class _SendViewState extends ConsumerState<SendView> {
address: _address!,
amount: amount,
isChange: false,
)
),
],
),
);
@ -725,7 +734,7 @@ class _SendViewState extends ConsumerState<SendView> {
address: _address!,
amount: amount,
isChange: false,
)
),
],
sparkRecipients: ref.read(pValidSparkSendToAddress)
? [
@ -734,7 +743,7 @@ class _SendViewState extends ConsumerState<SendView> {
amount: amount,
memo: memoController.text,
isChange: false,
)
),
]
: null,
),
@ -752,7 +761,7 @@ class _SendViewState extends ConsumerState<SendView> {
address: _address!,
amount: amount,
isChange: false,
)
),
],
memo: memo,
feeRateType: ref.read(feeRateTypeStateProvider),
@ -827,9 +836,10 @@ class _SendViewState extends ConsumerState<SendView> {
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
onPressed: () {
Navigator.of(context).pop();
@ -906,6 +916,10 @@ class _SendViewState extends ConsumerState<SendView> {
sendToController.text = _data!.contactLabel;
_address = _data!.address.trim();
_addressToggleFlag = true;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_setValidAddressProviders(_address);
});
}
if (isPaynymSend) {
@ -977,7 +991,8 @@ class _SendViewState extends ConsumerState<SendView> {
debugPrint("BUILD: $runtimeType");
final wallet = ref.watch(pWallets).getWallet(walletId);
final String locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
localeServiceChangeNotifierProvider.select((value) => value.locale),
);
final showCoinControl = wallet is CoinControlInterface &&
ref.watch(
@ -1029,7 +1044,7 @@ class _SendViewState extends ConsumerState<SendView> {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 50));
}
if (mounted) {
if (context.mounted) {
Navigator.of(context).pop();
}
},
@ -1114,82 +1129,93 @@ class _SendViewState extends ConsumerState<SendView> {
],
),
const Spacer(),
Builder(builder: (context) {
final Amount amount;
if (isFiro) {
switch (ref
.watch(
Builder(
builder: (context) {
final Amount amount;
if (isFiro) {
switch (ref
.watch(
publicPrivateBalanceStateProvider
.state)
.state) {
case FiroType.public:
amount = ref
.read(pWalletBalance(walletId))
.spendable;
break;
case FiroType.lelantus:
amount = ref
.read(pWalletBalanceSecondary(
walletId))
.spendable;
break;
case FiroType.spark:
amount = ref
.read(pWalletBalanceTertiary(
walletId))
.spendable;
break;
}
} else {
amount = ref
.read(pWalletBalance(walletId))
.spendable;
}
return GestureDetector(
onTap: () {
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
amount,
withUnitName: false,
);
},
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
Text(
ref
.watch(pAmountFormatter(coin))
.format(amount),
style: STextStyles.titleBold12(
context)
.copyWith(
fontSize: 10,
),
textAlign: TextAlign.right,
),
Text(
"${(amount.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount(
fractionDigits: 2,
).fiatString(
locale: locale,
)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}",
style:
STextStyles.subtitle(context)
.copyWith(
fontSize: 8,
),
textAlign: TextAlign.right,
.state,
)
],
.state) {
case FiroType.public:
amount = ref
.read(pWalletBalance(walletId))
.spendable;
break;
case FiroType.lelantus:
amount = ref
.read(
pWalletBalanceSecondary(
walletId,
),
)
.spendable;
break;
case FiroType.spark:
amount = ref
.read(
pWalletBalanceTertiary(
walletId,
),
)
.spendable;
break;
}
} else {
amount = ref
.read(pWalletBalance(walletId))
.spendable;
}
return GestureDetector(
onTap: () {
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
amount,
withUnitName: false,
);
},
child: Container(
color: Colors.transparent,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
Text(
ref
.watch(
pAmountFormatter(coin),
)
.format(amount),
style: STextStyles.titleBold12(
context,
).copyWith(
fontSize: 10,
),
textAlign: TextAlign.right,
),
Text(
"${(amount.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount(
fractionDigits: 2,
).fiatString(
locale: locale,
)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}",
style: STextStyles.subtitle(
context,
).copyWith(
fontSize: 8,
),
textAlign: TextAlign.right,
),
],
),
),
),
);
}),
);
},
),
],
),
),
@ -1291,12 +1317,14 @@ class _SendViewState extends ConsumerState<SendView> {
semanticsLabel:
"Clear Button. Clears The Address Field Input.",
key: const Key(
"sendViewClearAddressFieldButtonKey"),
"sendViewClearAddressFieldButtonKey",
),
onTap: () {
sendToController.text = "";
_address = "";
_setValidAddressProviders(
_address);
_address,
);
setState(() {
_addressToggleFlag =
false;
@ -1308,12 +1336,13 @@ class _SendViewState extends ConsumerState<SendView> {
semanticsLabel:
"Paste Button. Pastes From Clipboard To Address Field Input.",
key: const Key(
"sendViewPasteAddressFieldButtonKey"),
"sendViewPasteAddressFieldButtonKey",
),
onTap: () async {
final ClipboardData? data =
await clipboard.getData(
Clipboard
.kTextPlain);
Clipboard.kTextPlain,
);
if (data?.text != null &&
data!
.text!.isNotEmpty) {
@ -1323,23 +1352,27 @@ class _SendViewState extends ConsumerState<SendView> {
.contains("\n")) {
content =
content.substring(
0,
content.indexOf(
"\n"));
0,
content.indexOf(
"\n",
),
);
}
if (coin ==
Coin.epicCash) {
// strip http:// and https:// if content contains @
content = formatAddress(
content);
content,
);
}
sendToController.text =
content.trim();
_address = content.trim();
_setValidAddressProviders(
_address);
_address,
);
setState(() {
_addressToggleFlag =
sendToController
@ -1358,7 +1391,8 @@ class _SendViewState extends ConsumerState<SendView> {
semanticsLabel:
"Address Book Button. Opens Address Book For Address Field.",
key: const Key(
"sendViewAddressBookButtonKey"),
"sendViewAddressBookButtonKey",
),
onTap: () {
Navigator.of(context).pushNamed(
AddressBookView.routeName,
@ -1372,7 +1406,8 @@ class _SendViewState extends ConsumerState<SendView> {
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: const Key(
"sendViewScanQrButtonKey"),
"sendViewScanQrButtonKey",
),
onTap: _scanQr,
child: const QrCodeIcon(),
),
@ -1389,7 +1424,8 @@ class _SendViewState extends ConsumerState<SendView> {
if (isStellar ||
(ref.watch(pValidSparkSendToAddress) &&
ref.watch(
publicPrivateBalanceStateProvider) !=
publicPrivateBalanceStateProvider,
) !=
FiroType.lelantus))
ClipRRect(
borderRadius: BorderRadius.circular(
@ -1436,7 +1472,8 @@ class _SendViewState extends ConsumerState<SendView> {
semanticsLabel:
"Clear Button. Clears The Memo Field Input.",
key: const Key(
"sendViewClearMemoFieldButtonKey"),
"sendViewClearMemoFieldButtonKey",
),
onTap: () {
memoController.text = "";
setState(() {});
@ -1447,16 +1484,17 @@ class _SendViewState extends ConsumerState<SendView> {
semanticsLabel:
"Paste Button. Pastes From Clipboard To Memo Field Input.",
key: const Key(
"sendViewPasteMemoFieldButtonKey"),
"sendViewPasteMemoFieldButtonKey",
),
onTap: () async {
final ClipboardData? data =
await clipboard.getData(
Clipboard
.kTextPlain);
Clipboard.kTextPlain,
);
if (data?.text != null &&
data!
.text!.isNotEmpty) {
String content =
final String content =
data.text!.trim();
memoController.text =
@ -1482,13 +1520,15 @@ class _SendViewState extends ConsumerState<SendView> {
error = null;
} else if (isFiro) {
if (ref.watch(
publicPrivateBalanceStateProvider) ==
publicPrivateBalanceStateProvider,
) ==
FiroType.lelantus) {
if (_data != null &&
_data!.contactLabel == _address) {
error = SparkInterface.validateSparkAddress(
address: _data!.address,
isTestNet: coin.isTestNet)
address: _data!.address,
isTestNet: coin.isTestNet,
)
? "Unsupported"
: null;
} else if (ref
@ -1607,51 +1647,65 @@ class _SendViewState extends ConsumerState<SendView> {
Text(
"${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance",
style: STextStyles.itemSubtitle12(
context),
context,
),
),
const SizedBox(
width: 10,
),
Builder(builder: (_) {
final Amount amount;
switch (ref
.read(
Builder(
builder: (_) {
final Amount amount;
switch (ref
.read(
publicPrivateBalanceStateProvider
.state)
.state) {
case FiroType.public:
amount = ref
.watch(pWalletBalance(
walletId))
.spendable;
break;
case FiroType.lelantus:
amount = ref
.watch(
.state,
)
.state) {
case FiroType.public:
amount = ref
.watch(
pWalletBalance(
walletId,
),
)
.spendable;
break;
case FiroType.lelantus:
amount = ref
.watch(
pWalletBalanceSecondary(
walletId))
.spendable;
break;
case FiroType.spark:
amount = ref
.watch(
walletId,
),
)
.spendable;
break;
case FiroType.spark:
amount = ref
.watch(
pWalletBalanceTertiary(
walletId))
.spendable;
break;
}
walletId,
),
)
.spendable;
break;
}
return Text(
ref
.watch(
pAmountFormatter(coin))
.format(
amount,
),
style: STextStyles.itemSubtitle(
context),
);
}),
return Text(
ref
.watch(
pAmountFormatter(coin),
)
.format(
amount,
),
style:
STextStyles.itemSubtitle(
context,
),
);
},
),
],
),
SvgPicture.asset(
@ -1665,7 +1719,7 @@ class _SendViewState extends ConsumerState<SendView> {
],
),
),
)
),
],
),
const SizedBox(
@ -1687,8 +1741,9 @@ class _SendViewState extends ConsumerState<SendView> {
final Amount amount;
switch (ref
.read(
publicPrivateBalanceStateProvider
.state)
publicPrivateBalanceStateProvider
.state,
)
.state) {
case FiroType.public:
amount = ref
@ -1697,14 +1752,20 @@ class _SendViewState extends ConsumerState<SendView> {
break;
case FiroType.lelantus:
amount = ref
.read(pWalletBalanceSecondary(
walletId))
.read(
pWalletBalanceSecondary(
walletId,
),
)
.spendable;
break;
case FiroType.spark:
amount = ref
.read(pWalletBalanceTertiary(
walletId))
.read(
pWalletBalanceTertiary(
walletId,
),
)
.spendable;
break;
}
@ -1789,9 +1850,10 @@ class _SendViewState extends ConsumerState<SendView> {
.unitForCoin(coin),
style: STextStyles.smallMed14(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
@ -1851,13 +1913,16 @@ class _SendViewState extends ConsumerState<SendView> {
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
ref.watch(prefsChangeNotifierProvider
.select((value) => value.currency)),
ref.watch(
prefsChangeNotifierProvider
.select((value) => value.currency),
),
style: STextStyles.smallMed14(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
@ -1894,7 +1959,7 @@ class _SendViewState extends ConsumerState<SendView> {
);
}
if (mounted) {
if (context.mounted) {
final spendable = ref
.read(pWalletBalance(walletId))
.spendable;
@ -2092,8 +2157,9 @@ class _SendViewState extends ConsumerState<SendView> {
onPressed: isFiro &&
ref
.watch(
publicPrivateBalanceStateProvider
.state)
publicPrivateBalanceStateProvider
.state,
)
.state !=
FiroType.public
? null
@ -2113,8 +2179,9 @@ class _SendViewState extends ConsumerState<SendView> {
TransactionFeeSelectionSheet(
walletId: walletId,
amount: (Decimal.tryParse(
cryptoAmountController
.text) ??
cryptoAmountController
.text,
) ??
ref
.watch(pSendAmount)
?.decimal ??
@ -2150,8 +2217,9 @@ class _SendViewState extends ConsumerState<SendView> {
child: (isFiro &&
ref
.watch(
publicPrivateBalanceStateProvider
.state)
publicPrivateBalanceStateProvider
.state,
)
.state !=
FiroType.public)
? Row(
@ -2171,7 +2239,8 @@ class _SendViewState extends ConsumerState<SendView> {
"~${snapshot.data!}",
style: STextStyles
.itemSubtitle(
context),
context,
),
);
} else {
return AnimatedText(
@ -2183,7 +2252,8 @@ class _SendViewState extends ConsumerState<SendView> {
],
style: STextStyles
.itemSubtitle(
context),
context,
),
);
}
},
@ -2199,13 +2269,15 @@ class _SendViewState extends ConsumerState<SendView> {
Text(
ref
.watch(
feeRateTypeStateProvider
.state)
feeRateTypeStateProvider
.state,
)
.state
.prettyName,
style: STextStyles
.itemSubtitle12(
context),
context,
),
),
const SizedBox(
width: 10,
@ -2229,7 +2301,8 @@ class _SendViewState extends ConsumerState<SendView> {
: "~${snapshot.data!}",
style: STextStyles
.itemSubtitle(
context),
context,
),
);
} else {
return AnimatedText(
@ -2241,7 +2314,8 @@ class _SendViewState extends ConsumerState<SendView> {
],
style: STextStyles
.itemSubtitle(
context),
context,
),
);
}
},
@ -2259,7 +2333,7 @@ class _SendViewState extends ConsumerState<SendView> {
],
),
),
)
),
],
),
if (isCustomFee)

View file

@ -12,8 +12,8 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/themes/coin_image_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
@ -23,13 +23,15 @@ import 'package:stackwallet/widgets/stack_dialog.dart';
class BuildingTransactionDialog extends ConsumerStatefulWidget {
const BuildingTransactionDialog({
Key? key,
super.key,
required this.onCancel,
required this.coin,
}) : super(key: key);
required this.isSpark,
});
final VoidCallback onCancel;
final Coin coin;
final bool isSpark;
@override
ConsumerState<BuildingTransactionDialog> createState() =>
@ -62,13 +64,24 @@ class _RestoringDialogState extends ConsumerState<BuildingTransactionDialog> {
"Generating transaction",
style: STextStyles.desktopH3(context),
),
if (widget.isSpark)
const SizedBox(
height: 16,
),
if (widget.isSpark)
Text(
"This may take a few minutes...",
style: STextStyles.desktopSubtitleH2(context),
),
const SizedBox(
height: 40,
),
assetPath.endsWith(".gif")
? Image.file(File(
assetPath,
))
? Image.file(
File(
assetPath,
),
)
: const RotatingArrows(
width: 40,
height: 40,
@ -82,7 +95,7 @@ class _RestoringDialogState extends ConsumerState<BuildingTransactionDialog> {
onPressed: () {
onCancel.call();
},
)
),
],
);
} else {
@ -96,14 +109,26 @@ class _RestoringDialogState extends ConsumerState<BuildingTransactionDialog> {
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Image.file(File(
assetPath,
)),
Image.file(
File(
assetPath,
),
),
Text(
"Generating transaction",
textAlign: TextAlign.center,
style: STextStyles.pageTitleH2(context),
),
if (widget.isSpark)
const SizedBox(
height: 12,
),
if (widget.isSpark)
Text(
"This may take a few minutes...",
textAlign: TextAlign.center,
style: STextStyles.w500_16(context),
),
const SizedBox(
height: 32,
),
@ -124,7 +149,7 @@ class _RestoringDialogState extends ConsumerState<BuildingTransactionDialog> {
onCancel.call();
},
),
)
),
],
),
],
@ -132,6 +157,8 @@ class _RestoringDialogState extends ConsumerState<BuildingTransactionDialog> {
)
: StackDialog(
title: "Generating transaction",
message:
widget.isSpark ? "This may take a few minutes..." : null,
icon: const RotatingArrows(
width: 24,
height: 24,

View file

@ -173,85 +173,91 @@ class _FiroBalanceSelectionSheetState
),
),
),
const SizedBox(
height: 16,
),
GestureDetector(
onTap: () {
final state =
ref.read(publicPrivateBalanceStateProvider.state).state;
if (state != FiroType.lelantus) {
ref.read(publicPrivateBalanceStateProvider.state).state =
FiroType.lelantus;
}
Navigator.of(context).pop();
},
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: FiroType.lelantus,
groupValue: ref
.watch(
publicPrivateBalanceStateProvider.state)
.state,
onChanged: (x) {
ref
.read(publicPrivateBalanceStateProvider
.state)
.state = FiroType.lelantus;
Navigator.of(context).pop();
},
),
),
],
),
const SizedBox(
width: 12,
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
if (firoWallet.info.cachedBalanceSecondary.spendable.raw >
BigInt.zero)
const SizedBox(
height: 16,
),
if (firoWallet.info.cachedBalanceSecondary.spendable.raw >
BigInt.zero)
GestureDetector(
onTap: () {
final state = ref
.read(publicPrivateBalanceStateProvider.state)
.state;
if (state != FiroType.lelantus) {
ref
.read(publicPrivateBalanceStateProvider.state)
.state = FiroType.lelantus;
}
Navigator.of(context).pop();
},
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Row(
// children: [
Text(
"Lelantus balance",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
const SizedBox(
width: 2,
),
Text(
ref.watch(pAmountFormatter(coin)).format(
firoWallet.info.cachedBalanceSecondary
.spendable,
),
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.left,
SizedBox(
width: 20,
height: 20,
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: FiroType.lelantus,
groupValue: ref
.watch(publicPrivateBalanceStateProvider
.state)
.state,
onChanged: (x) {
ref
.read(publicPrivateBalanceStateProvider
.state)
.state = FiroType.lelantus;
Navigator.of(context).pop();
},
),
),
],
),
// ],
// ),
)
],
const SizedBox(
width: 12,
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Row(
// children: [
Text(
"Lelantus balance",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
const SizedBox(
width: 2,
),
Text(
ref.watch(pAmountFormatter(coin)).format(
firoWallet.info.cachedBalanceSecondary
.spendable,
),
style: STextStyles.itemSubtitle(context),
textAlign: TextAlign.left,
),
],
),
// ],
// ),
)
],
),
),
),
),
const SizedBox(
height: 16,
),

View file

@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart';
import 'package:stackwallet/widgets/animated_text.dart';
final feeSheetSessionCacheProvider =
@ -697,7 +698,7 @@ class _TransactionFeeSelectionSheetState
const SizedBox(
height: 24,
),
if (coin.isElectrumXCoin)
if (wallet is ElectrumXInterface)
GestureDetector(
onTap: () {
final state =
@ -766,7 +767,7 @@ class _TransactionFeeSelectionSheetState
),
),
),
if (coin.isElectrumXCoin)
if (wallet is ElectrumXInterface)
const SizedBox(
height: 24,
),

View file

@ -58,14 +58,14 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart';
class TokenSendView extends ConsumerStatefulWidget {
const TokenSendView({
Key? key,
super.key,
required this.walletId,
required this.coin,
required this.tokenContract,
this.autoFillData,
this.clipboard = const ClipboardWrapper(),
this.barcodeScanner = const BarcodeScannerWrapper(),
}) : super(key: key);
});
static const String routeName = "/tokenSendView";
@ -156,8 +156,10 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
// .state = true,
// );
Logging.instance.log("qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info);
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
final results = AddressUtils.parseUri(qrResult.rawContent);
@ -216,8 +218,9 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning);
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
}
@ -239,15 +242,19 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
? Amount.zero
: Amount.fromDecimal(
(baseAmount.decimal / _price).toDecimal(
scaleOnInfinitePrecision: tokenContract.decimals),
fractionDigits: tokenContract.decimals);
scaleOnInfinitePrecision: tokenContract.decimals,
),
fractionDigits: tokenContract.decimals,
);
}
if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) {
return;
}
_cachedAmountToSend = _amountToSend;
Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info);
Logging.instance.log(
"it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info,
);
_cryptoAmountChangeLock = true;
cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
@ -282,8 +289,10 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
return;
}
_cachedAmountToSend = _amountToSend;
Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info);
Logging.instance.log(
"it changed $_amountToSend $_cachedAmountToSend",
level: LogLevel.Info,
);
final price = ref
.read(priceAnd24hChangeNotifierProvider)
@ -457,6 +466,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
builder: (context) {
return BuildingTransactionDialog(
coin: wallet.info.coin,
isSpark: false,
onCancel: () {
wasCancelled = true;
@ -484,7 +494,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
address: _address!,
amount: amount,
isChange: false,
)
),
],
feeRateType: ref.read(feeRateTypeStateProvider),
note: noteController.text,
@ -502,20 +512,22 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
// pop building dialog
Navigator.of(context).pop();
unawaited(Navigator.of(context).push(
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => ConfirmTransactionView(
txData: txData,
walletId: walletId,
isTokenTx: true,
onSuccess: clearSendForm,
),
settings: const RouteSettings(
name: ConfirmTransactionView.routeName,
unawaited(
Navigator.of(context).push(
RouteGenerator.getRoute(
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => ConfirmTransactionView(
txData: txData,
walletId: walletId,
isTokenTx: true,
onSuccess: clearSendForm,
),
settings: const RouteSettings(
name: ConfirmTransactionView.routeName,
),
),
),
));
);
}
} catch (e) {
if (mounted) {
@ -538,9 +550,10 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
child: Text(
"Ok",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
onPressed: () {
Navigator.of(context).pop();
@ -626,7 +639,8 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
final String locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale));
localeServiceChangeNotifierProvider.select((value) => value.locale),
);
return Background(
child: Scaffold(
@ -638,7 +652,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 50));
}
if (mounted) {
if (context.mounted) {
Navigator.of(context).pop();
}
},
@ -712,11 +726,15 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
.watch(pAmountFormatter(coin))
.format(
ref
.read(pTokenBalance((
walletId: widget.walletId,
contractAddress:
tokenContract.address,
)))
.read(
pTokenBalance(
(
walletId: widget.walletId,
contractAddress:
tokenContract.address,
),
),
)
.spendable,
ethContract: tokenContract,
withUnitName: false,
@ -734,13 +752,17 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
.watch(pAmountFormatter(coin))
.format(
ref
.watch(pTokenBalance((
walletId:
widget.walletId,
contractAddress:
tokenContract
.address,
)))
.watch(
pTokenBalance(
(
walletId:
widget.walletId,
contractAddress:
tokenContract
.address,
),
),
)
.spendable,
ethContract: tokenContract,
),
@ -752,13 +774,17 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
textAlign: TextAlign.right,
),
Text(
"${(ref.watch(pTokenBalance((
"${(ref.watch(
pTokenBalance(
(
walletId:
widget.walletId,
contractAddress:
tokenContract
.address,
))).spendable.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount(
),
),
).spendable.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount(
fractionDigits: 2,
).fiatString(
locale: locale,
@ -768,7 +794,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
fontSize: 8,
),
textAlign: TextAlign.right,
)
),
],
),
),
@ -807,7 +833,9 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
onChanged: (newValue) {
_address = newValue.trim();
_updatePreviewButtonState(
_address, _amountToSend);
_address,
_amountToSend,
);
setState(() {
_addressToggleFlag = newValue.isNotEmpty;
@ -838,12 +866,15 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
_addressToggleFlag
? TextFieldIconButton(
key: const Key(
"tokenSendViewClearAddressFieldButtonKey"),
"tokenSendViewClearAddressFieldButtonKey",
),
onTap: () {
sendToController.text = "";
_address = "";
_updatePreviewButtonState(
_address, _amountToSend);
_address,
_amountToSend,
);
setState(() {
_addressToggleFlag = false;
});
@ -852,7 +883,8 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
)
: TextFieldIconButton(
key: const Key(
"tokenSendViewPasteAddressFieldButtonKey"),
"tokenSendViewPasteAddressFieldButtonKey",
),
onTap:
_onTokenSendViewPasteAddressFieldButtonPressed,
child: sendToController
@ -863,7 +895,8 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
if (sendToController.text.isEmpty)
TextFieldIconButton(
key: const Key(
"sendViewAddressBookButtonKey"),
"sendViewAddressBookButtonKey",
),
onTap: () {
Navigator.of(context).pushNamed(
AddressBookView.routeName,
@ -875,11 +908,12 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
if (sendToController.text.isEmpty)
TextFieldIconButton(
key: const Key(
"sendViewScanQrButtonKey"),
"sendViewScanQrButtonKey",
),
onTap:
_onTokenSendViewScanQrButtonPressed,
child: const QrCodeIcon(),
)
),
],
),
),
@ -997,9 +1031,10 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
tokenContract.symbol,
style: STextStyles.smallMed14(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
@ -1058,13 +1093,16 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
ref.watch(prefsChangeNotifierProvider
.select((value) => value.currency)),
ref.watch(
prefsChangeNotifierProvider
.select((value) => value.currency),
),
style: STextStyles.smallMed14(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
@ -1169,8 +1207,8 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
walletId: walletId,
isToken: true,
amount: (Decimal.tryParse(
cryptoAmountController
.text) ??
cryptoAmountController.text,
) ??
Decimal.zero)
.toAmount(
fractionDigits:
@ -1193,12 +1231,15 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
children: [
Text(
ref
.watch(feeRateTypeStateProvider
.state)
.watch(
feeRateTypeStateProvider
.state,
)
.state
.prettyName,
style: STextStyles.itemSubtitle12(
context),
context,
),
),
const SizedBox(
width: 10,
@ -1213,7 +1254,8 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
"~${snapshot.data!}",
style:
STextStyles.itemSubtitle(
context),
context,
),
);
} else {
return AnimatedText(
@ -1225,7 +1267,8 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
],
style:
STextStyles.itemSubtitle(
context),
context,
),
);
}
},
@ -1243,7 +1286,7 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
],
),
),
)
),
],
),
const Spacer(),
@ -1253,13 +1296,15 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
TextButton(
onPressed: ref
.watch(
previewTokenTxButtonStateProvider.state)
previewTokenTxButtonStateProvider.state,
)
.state
? _previewTransaction
: null,
style: ref
.watch(
previewTokenTxButtonStateProvider.state)
previewTokenTxButtonStateProvider.state,
)
.state
? Theme.of(context)
.extension<StackColors>()!

View file

@ -13,23 +13,21 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/debug_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/onetime_popups/tor_has_been_add_dialog.dart';
import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class HiddenSettings extends StatelessWidget {
const HiddenSettings({Key? key}) : super(key: key);
const HiddenSettings({super.key});
static const String routeName = "/hiddenSettings";
@ -39,27 +37,25 @@ class HiddenSettings extends StatelessWidget {
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: Util.isDesktop
? Padding(
padding: const EdgeInsets.all(8.0),
child: AppBarIconButton(
size: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: Navigator.of(context).pop,
),
)
: Container(),
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: AppBarIconButton(
size: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.arrowLeft,
width: 18,
height: 18,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: Navigator.of(context).pop,
),
),
title: Text(
"Dev options",
style: STextStyles.navBarTitle(context),
@ -176,49 +172,48 @@ class HiddenSettings extends StatelessWidget {
const SizedBox(
height: 12,
),
Consumer(builder: (_, ref, __) {
return GestureDetector(
onTap: () async {
await showOneTimeTorHasBeenAddedDialogIfRequired(
context,
);
},
child: RoundedWhiteContainer(
child: Text(
"Test tor stacy popup",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
);
}),
const SizedBox(
height: 12,
),
Consumer(builder: (_, ref, __) {
return GestureDetector(
onTap: () async {
final box = await Hive.openBox<bool>(
DB.boxNameOneTimeDialogsShown);
await box.clear();
},
child: RoundedWhiteContainer(
child: Text(
"Reset tor stacy popup",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
);
}),
const SizedBox(
height: 12,
),
// Consumer(builder: (_, ref, __) {
// return GestureDetector(
// onTap: () async {
// await showOneTimeTorHasBeenAddedDialogIfRequired(
// context,
// );
// },
// child: RoundedWhiteContainer(
// child: Text(
// "Test tor stacy popup",
// style: STextStyles.button(context).copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .accentColorDark),
// ),
// ),
// );
// }),
// const SizedBox(
// height: 12,
// ),
// Consumer(builder: (_, ref, __) {
// return GestureDetector(
// onTap: () async {
// final box = await Hive.openBox<bool>(
// DB.boxNameOneTimeDialogsShown);
// await box.clear();
// },
// child: RoundedWhiteContainer(
// child: Text(
// "Reset tor stacy popup",
// style: STextStyles.button(context).copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .accentColorDark),
// ),
// ),
// );
// }),
// const SizedBox(
// height: 12,
// ),
Consumer(
builder: (_, ref, __) {
if (ref.watch(prefsChangeNotifierProvider
@ -252,6 +247,35 @@ class HiddenSettings extends StatelessWidget {
}
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
return GestureDetector(
onTap: () async {
await showDialog<bool>(
context: context,
builder: (_) => TorWarningDialog(
coin: Coin.stellar,
),
);
},
child: RoundedWhiteContainer(
child: Text(
"Show Tor warning popup",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
return GestureDetector(

View file

@ -14,18 +14,21 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:solana/solana.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_eth_node_connection.dart';
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
import 'package:stackwallet/utilities/test_stellar_node_connection.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -166,22 +169,23 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
case Coin.firo:
case Coin.namecoin:
case Coin.particl:
case Coin.peercoin:
case Coin.peercoinTestNet:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.firoTestNet:
case Coin.dogecoinTestNet:
final client = ElectrumXClient(
host: formData.host!,
port: formData.port!,
useSSL: formData.useSSL!,
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
coin: coin,
);
try {
testPassed = await client.ping();
testPassed = await checkElectrumServer(
host: formData.host!,
port: formData.port!,
useSSL: formData.useSSL!,
overridePrefs: ref.read(prefsChangeNotifierProvider),
overrideTorService: ref.read(pTorService),
);
} catch (_) {
testPassed = false;
}
@ -189,14 +193,13 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
break;
case Coin.ethereum:
// TODO fix this
// final client = Web3Client(
// "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba",
// Client());
try {
// await client.getSyncStatus();
} catch (_) {}
testPassed = await testEthNodeConnection(formData.host!);
} catch (_) {
testPassed = false;
}
break;
case Coin.stellar:
case Coin.stellarTestnet:
try {
@ -216,6 +219,21 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
);
} catch (_) {}
break;
case Coin.solana:
try {
RpcClient rpcClient;
if (formData.host!.startsWith("http") ||
formData.host!.startsWith("https")) {
rpcClient = RpcClient("${formData.host}:${formData.port}");
} else {
rpcClient = RpcClient("http://${formData.host}:${formData.port}");
}
await rpcClient.getEpochInfo().then((value) => testPassed = true);
} catch (_) {
testPassed = false;
}
break;
}
if (showFlushBar && mounted) {
@ -746,6 +764,8 @@ class _NodeFormState extends ConsumerState<NodeForm> {
case Coin.namecoin:
case Coin.bitcoincash:
case Coin.particl:
case Coin.peercoin:
case Coin.peercoinTestNet:
case Coin.tezos:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
@ -756,8 +776,11 @@ class _NodeFormState extends ConsumerState<NodeForm> {
case Coin.nano:
case Coin.banano:
case Coin.eCash:
case Coin.solana:
case Coin.stellar:
case Coin.stellarTestnet:
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
return false;
case Coin.ethereum:

View file

@ -13,13 +13,15 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:solana/solana.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/connection_check/electrum_connection_check.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
@ -140,6 +142,8 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
case Coin.dogecoin:
case Coin.firo:
case Coin.particl:
case Coin.peercoin:
case Coin.peercoinTestNet:
case Coin.bitcoinTestNet:
case Coin.firoTestNet:
case Coin.dogecoinTestNet:
@ -148,17 +152,16 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
case Coin.eCash:
final client = ElectrumXClient(
host: node!.host,
port: node.port,
useSSL: node.useSSL,
failovers: [],
prefs: ref.read(prefsChangeNotifierProvider),
coin: coin,
);
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
try {
testPassed = await client.ping();
testPassed = await checkElectrumServer(
host: node!.host,
port: node.port,
useSSL: node.useSSL,
overridePrefs: ref.read(prefsChangeNotifierProvider),
overrideTorService: ref.read(pTorService),
);
} catch (_) {
testPassed = false;
}
@ -193,6 +196,20 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
testPassed = false;
}
break;
case Coin.solana:
try {
RpcClient rpcClient;
if (node!.host.startsWith("http") || node.host.startsWith("https")) {
rpcClient = RpcClient("${node.host}:${node.port}");
} else {
rpcClient = RpcClient("http://${node.host}:${node.port}");
}
await rpcClient.getEpochInfo().then((value) => testPassed = true);
} catch (_) {
testPassed = false;
}
break;
}
if (testPassed) {

View file

@ -88,7 +88,7 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
passwordFocusNode = FocusNode();
passwordRepeatFocusNode = FocusNode();
if (Platform.isAndroid) {
if (Platform.isAndroid || Platform.isIOS) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final dir = await stackFileSystem.prepareStorage();
if (mounted) {
@ -151,11 +151,11 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
const SizedBox(
height: 10,
),
if (!Platform.isAndroid)
if (!Platform.isAndroid && !Platform.isIOS)
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
onTap: Platform.isAndroid
onTap: Platform.isAndroid || Platform.isIOS
? null
: () async {
try {
@ -213,7 +213,7 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
),
onChanged: (newValue) {},
),
if (!Platform.isAndroid)
if (!Platform.isAndroid && !Platform.isIOS)
const SizedBox(
height: 10,
),

View file

@ -80,7 +80,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
passwordFocusNode = FocusNode();
passwordRepeatFocusNode = FocusNode();
if (Platform.isAndroid) {
if (Platform.isAndroid || Platform.isIOS) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final dir = await stackFileSystem.prepareStorage();
if (mounted) {
@ -179,14 +179,14 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!Platform.isAndroid)
if (!Platform.isAndroid && !Platform.isIOS)
Consumer(builder: (context, ref, __) {
return Container(
color: Colors.transparent,
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
onTap: Platform.isAndroid
onTap: Platform.isAndroid || Platform.isIOS
? null
: () async {
try {
@ -248,7 +248,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
),
);
}),
if (!Platform.isAndroid)
if (!Platform.isAndroid && !Platform.isIOS)
SizedBox(
height: !isDesktop ? 8 : 24,
),

View file

@ -260,7 +260,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
passwordFocusNode = FocusNode();
passwordRepeatFocusNode = FocusNode();
if (Platform.isAndroid) {
if (Platform.isAndroid || Platform.isIOS) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final dir = await stackFileSystem.prepareStorage();
if (mounted) {
@ -346,11 +346,11 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
const SizedBox(
height: 10,
),
if (!Platform.isAndroid)
if (!Platform.isAndroid && !Platform.isIOS)
TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
onTap: Platform.isAndroid
onTap: Platform.isAndroid || Platform.isIOS
? null
: () async {
try {
@ -418,7 +418,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
),
textAlign: TextAlign.left,
),
if (!Platform.isAndroid)
if (!Platform.isAndroid && !Platform.isIOS)
const SizedBox(
height: 10,
),

View file

@ -13,6 +13,7 @@ import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:frostdart/frostdart.dart' as frost;
import 'package:isar/isar.dart';
import 'package:stack_wallet_backup/stack_wallet_backup.dart';
import 'package:stackwallet/db/hive/db.dart';
@ -26,6 +27,7 @@ import 'package:stackwallet/models/stack_restoring_ui_state.dart';
import 'package:stackwallet/models/trade_wallet_lookup.dart';
import 'package:stackwallet/models/wallet_restore_state.dart';
import 'package:stackwallet/services/address_book_service.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/trade_notes_service.dart';
import 'package:stackwallet/services/trade_sent_from_stack_service.dart';
@ -41,7 +43,9 @@ import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
@ -302,6 +306,24 @@ abstract class SWB {
await wallet.getMnemonicPassphrase();
} else if (wallet is PrivateKeyInterface) {
backupWallet['privateKey'] = await wallet.getPrivateKey();
} else if (wallet is BitcoinFrostWallet) {
String? keys = await wallet.getSerializedKeys();
String? config = await wallet.getMultisigConfig();
if (keys == null || config == null) {
String err = "${wallet.info.coin.name} wallet ${wallet.info.name} "
"has null keys or config";
Logging.instance.log(err, level: LogLevel.Fatal);
throw Exception(err);
}
//This case should never actually happen in practice unless the whole
// wallet is somehow corrupt
// TODO [prio=low]: solve case in which either keys or config is null.
// Format keys & config as a JSON string and set otherDataJsonString.
Map<String, dynamic> frostData = {};
frostData["keys"] = keys;
frostData["config"] = config;
backupWallet['frostWalletData'] = jsonEncode(frostData);
}
backupWallet['coinName'] = wallet.info.coin.name;
backupWallet['storedChainHeight'] = wallet.info.cachedChainHeight;
@ -384,7 +406,9 @@ abstract class SWB {
if (walletbackup['mnemonic'] == null) {
// probably private key based
privateKey = walletbackup['privateKey'] as String;
if (walletbackup['privateKey'] != null) {
privateKey = walletbackup['privateKey'] as String;
}
} else {
if (walletbackup['mnemonic'] is List) {
List<String> mnemonicList = (walletbackup['mnemonic'] as List<dynamic>)
@ -406,6 +430,37 @@ abstract class SWB {
);
try {
String? serializedKeys;
String? multisigConfig;
if (info.coin.isFrost) {
// Decode info.otherDataJsonString for Frost recovery info.
final frostData = jsonDecode(walletbackup["frostWalletData"] as String);
serializedKeys = frostData["keys"] as String;
multisigConfig = frostData["config"] as String;
final myNameIndex = frost.getParticipantIndexFromKeys(
serializedKeys: serializedKeys,
);
final participants = Frost.getParticipants(
multisigConfig: multisigConfig,
);
final myName = participants[myNameIndex];
final frostInfo = FrostWalletInfo(
walletId: info.walletId,
knownSalts: [],
participants: participants,
myName: myName,
threshold: frost.multisigThreshold(
multisigConfig: multisigConfig,
),
);
await MainDB.instance.isar.writeTxn(() async {
await MainDB.instance.isar.frostWalletInfo.put(frostInfo);
});
}
final wallet = await Wallet.create(
walletInfo: info,
mainDB: MainDB.instance,
@ -427,7 +482,15 @@ abstract class SWB {
Future<void>? restoringFuture;
if (!(wallet is CwBasedInterface || wallet is EpiccashWallet)) {
restoringFuture = wallet.recover(isRescan: false);
if (wallet is BitcoinFrostWallet) {
restoringFuture = wallet.recover(
isRescan: false,
multisigConfig: multisigConfig!,
serializedKeys: serializedKeys!,
);
} else {
restoringFuture = wallet.recover(isRescan: false);
}
}
uiState?.update(

View file

@ -79,11 +79,16 @@ class SWBFileSystem {
}
Future<void> pickDir(BuildContext context) async {
final String? path = await FilePicker.platform.getDirectoryPath(
dialogTitle: "Choose Backup location",
initialDirectory: startPath!.path,
lockParentWindow: true,
);
final String? path;
if (Platform.isIOS) {
path = startPath?.path;
} else {
path = await FilePicker.platform.getDirectoryPath(
dialogTitle: "Choose Backup location",
initialDirectory: startPath!.path,
lockParentWindow: true,
);
}
dirPath = path;
}

View file

@ -11,7 +11,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart';
import 'package:stackwallet/providers/global/active_wallet_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -95,12 +94,12 @@ class SyncingOptionsView extends ConsumerWidget {
ref.read(prefsChangeNotifierProvider).syncType =
SyncingType.currentWalletOnly;
// disable auto sync on all wallets that aren't active/current
ref.read(pWallets).wallets.forEach((e) {
if (e.walletId != ref.read(currentWalletIdProvider)) {
e.shouldAutoSync = false;
}
});
// // disable auto sync on all wallets that aren't active/current
// ref.read(pWallets).wallets.forEach((e) {
// if (e.walletId != ref.read(currentWalletIdProvider)) {
// e.shouldAutoSync = false;
// }
// });
}
},
child: Container(
@ -174,11 +173,11 @@ class SyncingOptionsView extends ConsumerWidget {
ref.read(prefsChangeNotifierProvider).syncType =
SyncingType.allWalletsOnStartup;
// enable auto sync on all wallets
ref
.read(pWallets)
.wallets
.forEach((e) => e.shouldAutoSync = true);
// // enable auto sync on all wallets
// ref
// .read(pWallets)
// .wallets
// .forEach((e) => e.shouldAutoSync = true);
}
},
child: Container(
@ -252,13 +251,13 @@ class SyncingOptionsView extends ConsumerWidget {
ref.read(prefsChangeNotifierProvider).syncType =
SyncingType.selectedWalletsAtStartup;
final ids = ref
.read(prefsChangeNotifierProvider)
.walletIdsSyncOnStartup;
// enable auto sync on selected wallets only
ref.read(pWallets).wallets.forEach(
(e) => e.shouldAutoSync = ids.contains(e.walletId));
// final ids = ref
// .read(prefsChangeNotifierProvider)
// .walletIdsSyncOnStartup;
//
// // enable auto sync on selected wallets only
// ref.read(pWallets).wallets.forEach(
// (e) => e.shouldAutoSync = ids.contains(e.walletId));
}
},
child: Container(

View file

@ -13,13 +13,11 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/providers/global/active_wallet_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
@ -181,9 +179,9 @@ class WalletSyncingOptionsView extends ConsumerWidget {
.walletIdsSyncOnStartup))
.contains(info.walletId),
onValueChanged: (value) {
final syncType = ref
.read(prefsChangeNotifierProvider)
.syncType;
// final syncType = ref
// .read(prefsChangeNotifierProvider)
// .syncType;
final ids = ref
.read(prefsChangeNotifierProvider)
.walletIdsSyncOnStartup
@ -194,25 +192,25 @@ class WalletSyncingOptionsView extends ConsumerWidget {
ids.remove(info.walletId);
}
final wallet = ref
.read(pWallets)
.getWallet(info.walletId);
switch (syncType) {
case SyncingType.currentWalletOnly:
if (info.walletId ==
ref.read(
currentWalletIdProvider)) {
wallet.shouldAutoSync = value;
}
break;
case SyncingType
.selectedWalletsAtStartup:
case SyncingType
.allWalletsOnStartup:
wallet.shouldAutoSync = value;
break;
}
// final wallet = ref
// .read(pWallets)
// .getWallet(info.walletId);
//
// switch (syncType) {
// case SyncingType.currentWalletOnly:
// if (info.walletId ==
// ref.read(
// currentWalletIdProvider)) {
// wallet.shouldAutoSync = value;
// }
// break;
// case SyncingType
// .selectedWalletsAtStartup:
// case SyncingType
// .allWalletsOnStartup:
// wallet.shouldAutoSync = value;
// break;
// }
ref
.read(prefsChangeNotifierProvider)

View file

@ -16,17 +16,19 @@ import 'package:stackwallet/utilities/text_styles.dart';
class SettingsListButton extends StatelessWidget {
const SettingsListButton({
Key? key,
super.key,
required this.iconAssetName,
required this.title,
this.onPressed,
this.iconSize = 20.0,
}) : super(key: key);
this.padding = const EdgeInsets.all(8.0),
});
final String iconAssetName;
final String title;
final VoidCallback? onPressed;
final double iconSize;
final EdgeInsetsGeometry padding;
@override
Widget build(BuildContext context) {
@ -44,7 +46,7 @@ class SettingsListButton extends StatelessWidget {
),
onPressed: onPressed,
child: Padding(
padding: const EdgeInsets.all(8.0),
padding: padding,
child: Row(
children: [
Container(

View file

@ -0,0 +1,181 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/initiate_resharing_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/frost_scaffold.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class FrostMSWalletOptionsView extends ConsumerWidget {
const FrostMSWalletOptionsView({
super.key,
required this.walletId,
});
static const String routeName = "/frostMSWalletOptionsView";
final String walletId;
static const _padding = 12.0;
@override
Widget build(BuildContext context, WidgetRef ref) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"FROST Multisig options",
style: STextStyles.navBarTitle(context),
),
),
body: child),
),
child: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 16,
right: 16,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
padding: EdgeInsets.zero,
child: SettingsListButton(
padding: const EdgeInsets.all(_padding),
title: "Show participants",
iconAssetName: Assets.svg.peers,
onPressed: () {
Navigator.of(context).pushNamed(
FrostParticipantsView.routeName,
arguments: walletId,
);
},
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
padding: EdgeInsets.zero,
child: SettingsListButton(
padding: const EdgeInsets.all(_padding),
title: "Initiate resharing",
iconAssetName: Assets.svg.swap2,
onPressed: () {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(walletId)!;
ref.read(pFrostMyName.state).state = frostInfo.myName;
Navigator.of(context).pushNamed(
InitiateResharingView.routeName,
arguments: walletId,
);
},
),
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
padding: EdgeInsets.zero,
child: SettingsListButton(
padding: const EdgeInsets.all(_padding),
title: "Import reshare config",
iconAssetName: Assets.svg.downloadFolder,
iconSize: 16,
onPressed: () {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(walletId)!;
ref.read(pFrostMyName.state).state = frostInfo.myName;
final wallet = ref.read(pWallets).getWallet(walletId)
as BitcoinFrostWallet;
ref.read(pFrostScaffoldArgs.state).state = (
info: (
walletName: wallet.info.name,
frostCurrency: wallet.cryptoCurrency,
),
walletId: wallet.walletId,
stepRoutes: FrostRouteGenerator.importReshareStepRoutes,
parentNav: Navigator.of(context),
frostInterruptionDialogType:
FrostInterruptionDialogType.resharing,
callerRouteName: FrostMSWalletOptionsView.routeName,
);
Navigator.of(context).pushNamed(
FrostStepScaffold.routeName,
);
},
),
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class FrostParticipantsView extends ConsumerWidget {
const FrostParticipantsView({
super.key,
required this.walletId,
});
static const String routeName = "/frostParticipantsView";
final String walletId;
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(walletId)!;
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Participants",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
crossAxisAlignment: Util.isDesktop
? CrossAxisAlignment.start
: CrossAxisAlignment.stretch,
children: [
for (int i = 0; i < frostInfo.participants.length; i++)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 5,
),
child: RoundedWhiteContainer(
child: Row(
children: [
Container(
width: 26,
height: 26,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveBG,
borderRadius: BorderRadius.circular(
200,
),
),
child: Center(
child: SvgPicture.asset(
Assets.svg.user,
width: 16,
height: 16,
),
),
),
const SizedBox(
width: 8,
),
Expanded(
child: Text(
frostInfo.participants[i] == frostInfo.myName
? "${frostInfo.participants[i]} (me)"
: frostInfo.participants[i],
style: STextStyles.w500_14(context),
),
),
const SizedBox(
width: 8,
),
IconCopyButton(
data: frostInfo.participants[i],
),
],
),
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,499 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:frostdart/frostdart.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/frost_scaffold.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
final class CompleteReshareConfigView extends ConsumerStatefulWidget {
const CompleteReshareConfigView({
super.key,
required this.walletId,
required this.resharers,
});
static const String routeName = "/completeReshareConfigView";
final String walletId;
final Map<String, int> resharers;
@override
ConsumerState<CompleteReshareConfigView> createState() =>
_CompleteReshareConfigViewState();
}
class _CompleteReshareConfigViewState
extends ConsumerState<CompleteReshareConfigView> {
final _newThresholdController = TextEditingController();
final _newParticipantsCountController = TextEditingController();
final List<TextEditingController> controllers = [];
late final String myName;
int _participantsCount = 0;
bool _buttonLock = false;
bool _includeMeInReshare = false;
Future<void> _onPressed() async {
if (_buttonLock) {
return;
}
_buttonLock = true;
try {
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
final validationMessage = _validateInputData();
if (validationMessage != "valid") {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: validationMessage,
desktopPopRootNavigator: Util.isDesktop,
),
);
}
final List<String> newParticipants =
controllers.map((e) => e.text.trim()).toList();
if (_includeMeInReshare) {
newParticipants.insert(0, myName);
}
final config = Frost.createResharerConfig(
newThreshold: int.parse(_newThresholdController.text),
resharers: widget.resharers.values.toList(),
newParticipants: newParticipants,
);
final salt = Format.uint8listToString(
resharerSalt(resharerConfig: config),
);
if (frostInfo.knownSalts.contains(salt)) {
return await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: "Duplicate config salt",
desktopPopRootNavigator: Util.isDesktop,
),
);
} else {
final salts = frostInfo.knownSalts; // Fixed length list.
final newSalts = List<String>.from(salts)..add(salt);
final mainDB = ref.read(mainDBProvider);
await mainDB.isar.writeTxn(() async {
final info = frostInfo;
await mainDB.isar.frostWalletInfo.delete(info.id);
await mainDB.isar.frostWalletInfo.put(
info.copyWith(knownSalts: newSalts),
);
});
}
ref.read(pFrostResharingData).myName = myName;
ref.read(pFrostResharingData).resharerRConfig = Frost.encodeRConfig(
config,
widget.resharers,
);
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
if (mounted) {
ref.read(pFrostScaffoldArgs.state).state = (
info: (
walletName: wallet.info.name,
frostCurrency: wallet.cryptoCurrency,
),
walletId: wallet.walletId,
stepRoutes: FrostRouteGenerator.initiateReshareStepRoutes,
parentNav: Navigator.of(context),
frostInterruptionDialogType: FrostInterruptionDialogType.resharing,
callerRouteName: CompleteReshareConfigView.routeName,
);
await Navigator.of(context).pushNamed(
FrostStepScaffold.routeName,
);
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
} finally {
_buttonLock = false;
}
}
String _validateInputData() {
final threshold = int.tryParse(_newThresholdController.text);
if (threshold == null) {
return "Choose a threshold";
}
final partsCount = int.tryParse(_newParticipantsCountController.text);
if (partsCount == null) {
return "Choose total number of participants";
}
if (threshold > partsCount) {
return "Threshold cannot be greater than the number of participants";
}
if (partsCount < 2) {
return "At least two participants required";
}
final newParticipants = controllers.map((e) => e.text.trim()).toList();
if (newParticipants.contains(myName)) {
return "Using your own name should be done using the checkbox to include"
" yourself";
}
if (_includeMeInReshare) {
newParticipants.add(myName);
}
if (newParticipants.length != partsCount) {
return "Participants count error";
}
final hasEmptyParticipants = newParticipants
.map((e) => e.trim().isEmpty)
.reduce((value, element) => value |= element);
if (hasEmptyParticipants) {
return "Participants must not be empty";
}
if (newParticipants.length != newParticipants.toSet().length) {
return "Duplicate participant name found";
}
return "valid";
}
void _participantsCountChanged(String newValue) {
int? count = int.tryParse(newValue);
if (count != null) {
if (_includeMeInReshare) {
count = max(0, count - 1);
}
if (count > _participantsCount) {
for (int i = _participantsCount; i < count; i++) {
controllers.add(TextEditingController());
}
_participantsCount = count;
setState(() {});
} else if (count < _participantsCount) {
for (int i = _participantsCount; i > count; i--) {
final last = controllers.removeLast();
last.dispose();
}
_participantsCount = count;
setState(() {});
}
}
}
@override
void initState() {
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
myName = frostInfo.myName;
super.initState();
}
@override
void dispose() {
_newThresholdController.dispose();
_newParticipantsCountController.dispose();
for (final e in controllers) {
e.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Edit group details",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: Util.isDesktop
? CrossAxisAlignment.start
: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 8,
),
GestureDetector(
onTap: () {
setState(() {
_includeMeInReshare = !_includeMeInReshare;
});
_participantsCountChanged(_newParticipantsCountController.text);
},
child: Container(
color: Colors.transparent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 26,
child: Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
value: _includeMeInReshare,
onChanged: (value) {
setState(
() => _includeMeInReshare = value == true,
);
_participantsCountChanged(
_newParticipantsCountController.text,
);
},
),
),
const SizedBox(
width: 12,
),
Expanded(
child: Text(
"I will be a signer in the new config",
style: STextStyles.w500_14(context),
),
),
],
),
),
),
const SizedBox(
height: 20,
),
Text(
"New threshold",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _newThresholdController,
decoration: InputDecoration(
hintText: "Enter number of signatures",
hintStyle: STextStyles.fieldLabel(context),
),
),
const SizedBox(
height: 6,
),
RoundedWhiteContainer(
child: Text(
"Enter number of signatures required for fund management.",
style: STextStyles.w500_12(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textSubtitle2,
),
),
),
const SizedBox(
height: 16,
),
Text(
"New number of participants",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
),
const SizedBox(
height: 10,
),
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
controller: _newParticipantsCountController,
onChanged: _participantsCountChanged,
decoration: InputDecoration(
hintText: "Enter number of participants",
hintStyle: STextStyles.fieldLabel(context),
),
),
const SizedBox(
height: 6,
),
RoundedWhiteContainer(
child: Text(
"The number of participants must be equal to or less than the"
" number of required signatures.",
style: STextStyles.w500_12(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textSubtitle2,
),
),
),
const SizedBox(
height: 16,
),
if (controllers.isNotEmpty)
Text(
"Participants",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
),
if (controllers.isNotEmpty)
const SizedBox(
height: 10,
),
if (controllers.isNotEmpty)
RoundedWhiteContainer(
child: Text(
"Type each name in one word without spaces.",
style: STextStyles.w500_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle2,
),
),
),
if (controllers.isNotEmpty)
Column(
children: [
for (int i = 0; i < controllers.length; i++)
Padding(
padding: const EdgeInsets.only(
top: 10,
),
child: TextField(
controller: controllers[i],
decoration: InputDecoration(
hintText: "Enter name",
hintStyle: STextStyles.fieldLabel(context),
),
),
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Generate config",
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
}
await _onPressed();
},
),
],
),
),
);
}
}

View file

@ -0,0 +1,258 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/initiate_resharing/complete_reshare_config_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
final class InitiateResharingView extends ConsumerStatefulWidget {
const InitiateResharingView({
super.key,
required this.walletId,
});
static const String routeName = "/beginReshareConfigView";
final String walletId;
@override
ConsumerState<InitiateResharingView> createState() =>
_BeginReshareConfigViewState();
}
class _BeginReshareConfigViewState
extends ConsumerState<InitiateResharingView> {
late final String myName;
late final int currentThreshold;
late final List<String> originalParticipants;
late final List<String> currentParticipantsWithoutMe;
final Set<String> selectedParticipants = {};
@override
void initState() {
ref.read(pFrostResharingData).reset();
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
final frostInfo = ref
.read(mainDBProvider)
.isar
.frostWalletInfo
.getByWalletIdSync(widget.walletId)!;
currentThreshold = frostInfo.threshold;
originalParticipants = frostInfo.participants.toList(growable: false);
currentParticipantsWithoutMe = originalParticipants.toList();
// sanity check (should never actually fail, but very bad if it does)
assert(originalParticipants.length == currentParticipantsWithoutMe.length);
myName = frostInfo.myName;
currentParticipantsWithoutMe.remove(myName);
super.initState();
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: Util.isDesktop,
builder: (child) => DesktopScaffold(
background: Theme.of(context).extension<StackColors>()!.background,
appBar: const DesktopAppBar(
isCompactHeight: false,
leading: AppBarBackButton(),
trailing: ExitToMyStackButton(),
),
body: SizedBox(
width: 480,
child: child,
),
),
child: ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Initiate resharing",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
);
},
),
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Select group members who will participate in resharing.",
style: STextStyles.w600_12(context),
),
const SizedBox(
height: 10,
),
Text(
"You must have the threshold number of members (including you) to initiate resharing.",
style: STextStyles.w600_12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.customTextButtonEnabledText,
),
),
],
),
),
const SizedBox(
height: 16,
),
Column(
children: [
for (int i = 0; i < currentParticipantsWithoutMe.length; i++)
Padding(
padding: const EdgeInsets.only(
top: 10,
),
child: RoundedWhiteContainer(
padding: EdgeInsets.zero,
onPressed: () {
if (selectedParticipants
.contains(currentParticipantsWithoutMe[i])) {
selectedParticipants
.remove(currentParticipantsWithoutMe[i]);
} else {
selectedParticipants
.add(currentParticipantsWithoutMe[i]);
}
setState(() {});
},
child: Container(
color: Colors.transparent,
child: IgnorePointer(
child: Row(
children: [
Checkbox(
value: selectedParticipants
.contains(currentParticipantsWithoutMe[i]),
onChanged: (_) {},
),
const SizedBox(
width: 10,
),
Text(
currentParticipantsWithoutMe[i],
style: STextStyles.itemSubtitle12(context),
),
],
),
),
),
),
),
],
),
if (!Util.isDesktop) const Spacer(),
const SizedBox(
height: 16,
),
RoundedWhiteContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Required members",
style: STextStyles.w500_14(context).copyWith(
color:
Theme.of(context).extension<StackColors>()!.textDark3,
),
),
Text(
// +1 is included as the initiator who will also take part
"${selectedParticipants.length + 1} / $currentThreshold",
style: STextStyles.w500_14(context).copyWith(
color: selectedParticipants.length + 1 >= currentThreshold
? Theme.of(context)
.extension<StackColors>()!
.accentColorGreen
: Theme.of(context)
.extension<StackColors>()!
.accentColorRed,
),
),
],
),
),
const SizedBox(
height: 16,
),
PrimaryButton(
label: "Continue",
// +1 is included as the initiator who will also take part
enabled: selectedParticipants.length + 1 >= currentThreshold,
onPressed: () async {
// include self now
selectedParticipants.add(myName);
final Map<String, int> resharers = {};
for (final name in selectedParticipants) {
resharers[name] = originalParticipants.indexOf(name);
}
await Navigator.of(context).pushNamed(
CompleteReshareConfigView.routeName,
arguments: (
walletId: widget.walletId,
resharers: resharers,
),
);
},
),
],
),
),
);
}
}

View file

@ -17,34 +17,50 @@ import 'package:flutter_svg/svg.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/address_utils.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
import 'package:stackwallet/widgets/detail_item.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class WalletBackupView extends ConsumerWidget {
const WalletBackupView({
Key? key,
super.key,
required this.walletId,
required this.mnemonic,
this.frostWalletData,
this.clipboardInterface = const ClipboardWrapper(),
}) : super(key: key);
});
static const String routeName = "/walletBackup";
final String walletId;
final List<String> mnemonic;
final ({
String myName,
String config,
String keys,
({String config, String keys})? prevGen,
})? frostWalletData;
final ClipboardInterface clipboardInterface;
@override
Widget build(BuildContext context, WidgetRef ref) {
debugPrint("BUILD: $runtimeType");
final bool frost = frostWalletData != null;
final prevGen = frostWalletData?.prevGen != null;
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -91,139 +107,261 @@ class WalletBackupView extends ConsumerWidget {
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 4,
),
Text(
ref.watch(pWalletName(walletId)),
textAlign: TextAlign.center,
style: STextStyles.label(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 4,
),
Text(
"Recovery Phrase",
textAlign: TextAlign.center,
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 16,
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.",
style: STextStyles.label(context),
),
),
),
const SizedBox(
height: 8,
),
Expanded(
child: SingleChildScrollView(
child: MnemonicTable(
words: mnemonic,
isDesktop: false,
),
),
),
const SizedBox(
height: 12,
),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
String data = AddressUtils.encodeQRSeedData(mnemonic);
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (_) {
final width = MediaQuery.of(context).size.width / 2;
return StackDialogBase(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
"Recovery phrase QR code",
style: STextStyles.pageTitleH2(context),
),
),
const SizedBox(
height: 12,
),
Center(
child: RepaintBoundary(
// key: _qrKey,
child: SizedBox(
width: width + 20,
height: width + 20,
child: QrImageView(
data: data,
size: width,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.popupBG,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
child: frost
? LayoutBuilder(
builder: (builderContext, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 24,
),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
RoundedWhiteContainer(
child: Text(
"Please write down your backup data. Keep it safe and "
"never share it with anyone. "
"Your backup data is the only way you can access your "
"funds if you forget your PIN, lose your phone, etc."
"\n\n"
"Stack Wallet does not keep nor is able to restore "
"your backup data. "
"Only you have access to your wallet.",
style: STextStyles.label(context),
),
),
),
const SizedBox(
height: 12,
),
Center(
child: SizedBox(
width: width,
child: TextButton(
onPressed: () async {
// await _capturePng(true);
Navigator.of(context).pop();
},
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
const SizedBox(
height: 24,
),
// DetailItem(
// title: "My name",
// detail: frostWalletData!.myName,
// button: Util.isDesktop
// ? IconCopyButton(
// data: frostWalletData!.myName,
// )
// : SimpleCopyButton(
// data: frostWalletData!.myName,
// ),
// ),
// const SizedBox(
// height: 16,
// ),
DetailItem(
title: "Multisig config",
detail: frostWalletData!.config,
button: Util.isDesktop
? IconCopyButton(
data: frostWalletData!.config,
)
: SimpleCopyButton(
data: frostWalletData!.config,
),
),
const SizedBox(
height: 16,
),
DetailItem(
title: "Keys",
detail: frostWalletData!.keys,
button: Util.isDesktop
? IconCopyButton(
data: frostWalletData!.keys,
)
: SimpleCopyButton(
data: frostWalletData!.keys,
),
),
if (prevGen)
const SizedBox(
height: 24,
),
if (prevGen)
RoundedWhiteContainer(
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
"Previous generation info",
style: STextStyles.label(context),
),
),
),
),
],
if (prevGen)
const SizedBox(
height: 12,
),
if (prevGen)
DetailItem(
title: "Previous multisig config",
detail: frostWalletData!.prevGen!.config,
button: Util.isDesktop
? IconCopyButton(
data:
frostWalletData!.prevGen!.config,
)
: SimpleCopyButton(
data:
frostWalletData!.prevGen!.config,
),
),
if (prevGen)
const SizedBox(
height: 16,
),
if (prevGen)
DetailItem(
title: "Previous keys",
detail: frostWalletData!.prevGen!.keys,
button: Util.isDesktop
? IconCopyButton(
data: frostWalletData!.prevGen!.keys,
)
: SimpleCopyButton(
data: frostWalletData!.prevGen!.keys,
),
),
],
),
),
);
},
);
},
child: Text(
"Show QR Code",
style: STextStyles.button(context),
),
);
},
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 4,
),
Text(
ref.watch(pWalletName(walletId)),
textAlign: TextAlign.center,
style: STextStyles.label(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 4,
),
Text(
"Recovery Phrase",
textAlign: TextAlign.center,
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 16,
),
Container(
decoration: BoxDecoration(
color:
Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.",
style: STextStyles.label(context),
),
),
),
const SizedBox(
height: 8,
),
Expanded(
child: SingleChildScrollView(
child: MnemonicTable(
words: mnemonic,
isDesktop: false,
),
),
),
const SizedBox(
height: 12,
),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
String data = AddressUtils.encodeQRSeedData(mnemonic);
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (_) {
final width = MediaQuery.of(context).size.width / 2;
return StackDialogBase(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
"Recovery phrase QR code",
style: STextStyles.pageTitleH2(context),
),
),
const SizedBox(
height: 12,
),
Center(
child: RepaintBoundary(
// key: _qrKey,
child: SizedBox(
width: width + 20,
height: width + 20,
child: QrImageView(
data: data,
size: width,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.popupBG,
foregroundColor: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
const SizedBox(
height: 12,
),
Center(
child: SizedBox(
width: width,
child: TextButton(
onPressed: () async {
// await _capturePng(true);
Navigator.of(context).pop();
},
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(
context),
child: Text(
"Cancel",
style: STextStyles.button(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
),
),
],
),
);
},
);
},
child: Text(
"Show QR Code",
style: STextStyles.button(context),
),
),
],
),
),
],
),
),
),
);

View file

@ -22,6 +22,7 @@ import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
@ -39,6 +40,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import 'package:stackwallet/widgets/background.dart';
@ -51,13 +53,13 @@ import 'package:tuple/tuple.dart';
/// [eventBus] should only be set during testing
class WalletSettingsView extends ConsumerStatefulWidget {
const WalletSettingsView({
Key? key,
super.key,
required this.walletId,
required this.coin,
required this.initialSyncStatus,
required this.initialNodeStatus,
this.eventBus,
}) : super(key: key);
});
static const String routeName = "/walletSettings";
@ -204,6 +206,22 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
);
},
),
if (coin.isFrost)
const SizedBox(
height: 8,
),
if (coin.isFrost)
SettingsListButton(
iconAssetName: Assets.svg.addressBook2,
iconSize: 16,
title: "FROST Multisig settings",
onPressed: () {
Navigator.of(context).pushNamed(
FrostMSWalletOptionsView.routeName,
arguments: walletId,
);
},
),
const SizedBox(
height: 8,
),
@ -235,39 +253,79 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
final wallet = ref
.read(pWallets)
.getWallet(widget.walletId);
// TODO: [prio=frost] take wallets that don't have a mnemonic into account
if (wallet is MnemonicInterface) {
final mnemonic =
await wallet.getMnemonicAsWords();
if (mounted) {
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator
.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments:
Tuple2(
walletId, mnemonic),
showBackButton: true,
routeOnSuccess:
WalletBackupView
.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery phrase",
biometricsAuthenticationTitle:
"View recovery phrase",
),
settings: const RouteSettings(
name:
"/viewRecoverPhraseLockscreen"),
),
// TODO: [prio=med] take wallets that don't have a mnemonic into account
List<String>? mnemonic;
({
String myName,
String config,
String keys,
({
String config,
String keys
})? prevGen,
})? frostWalletData;
if (wallet is BitcoinFrostWallet) {
final futures = [
wallet.getSerializedKeys(),
wallet.getMultisigConfig(),
wallet.getSerializedKeysPrevGen(),
wallet.getMultisigConfigPrevGen(),
];
final results =
await Future.wait(futures);
if (results.length == 4) {
frostWalletData = (
myName: wallet.frostInfo.myName,
config: results[1]!,
keys: results[0]!,
prevGen: results[2] == null ||
results[3] == null
? null
: (
config: results[3]!,
keys: results[2]!,
),
);
}
} else if (wallet
is MnemonicInterface) {
mnemonic =
await wallet.getMnemonicAsWords();
}
if (context.mounted) {
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator
.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments: (
walletId: walletId,
mnemonic: mnemonic ?? [],
frostWalletData:
frostWalletData,
),
showBackButton: true,
routeOnSuccess:
WalletBackupView.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery phrase",
biometricsAuthenticationTitle:
"View recovery phrase",
),
settings: const RouteSettings(
name:
"/viewRecoverPhraseLockscreen"),
),
);
}
},
);

View file

@ -83,7 +83,7 @@ class _ChangeRepresentativeViewState
whileFuture: changeFuture(_textController.text),
context: context,
message: "Updating representative...",
isDesktop: Util.isDesktop,
rootNavigator: Util.isDesktop,
onException: (ex) {
String msg = ex.toString();
while (msg.isNotEmpty && msg.startsWith("Exception:")) {

View file

@ -23,7 +23,6 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart';
enum FiroRescanRecoveryErrorViewOption {
retry,
@ -269,8 +268,10 @@ class _FiroRescanRecoveryErrorViewState
shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments:
Tuple2(widget.walletId, mnemonic),
routeOnSuccessArguments: (
walletId: widget.walletId,
mnemonic: mnemonic,
),
showBackButton: true,
routeOnSuccess: WalletBackupView.routeName,
biometricsCancelButtonString: "CANCEL",

View file

@ -98,7 +98,7 @@ class _MyTokenSelectItemState extends ConsumerState<MyTokenSelectItem> {
final success = await showLoading<bool>(
whileFuture: _loadTokenWallet(context, ref),
context: context,
isDesktop: isDesktop,
rootNavigator: isDesktop,
message: "Loading ${widget.token.name}",
);

View file

@ -33,9 +33,9 @@ enum _BalanceType {
class WalletBalanceToggleSheet extends ConsumerWidget {
const WalletBalanceToggleSheet({
Key? key,
super.key,
required this.walletId,
}) : super(key: key);
});
final String walletId;
@ -46,7 +46,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
final coin = ref.watch(pWalletCoin(walletId));
final isFiro = coin == Coin.firo || coin == Coin.firoTestNet;
Balance balance = ref.watch(pWalletBalance(walletId));
final balance = ref.watch(pWalletBalance(walletId));
_BalanceType _bal =
ref.watch(walletBalanceToggleStateProvider.state).state ==
@ -77,6 +77,11 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
// already set above
break;
}
// hack to not show lelantus balance in ui if zero
if (balanceSecondary?.spendable.raw == BigInt.zero) {
balanceSecondary = null;
}
}
return Container(
@ -289,7 +294,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
class BalanceSelector<T> extends ConsumerWidget {
const BalanceSelector({
Key? key,
super.key,
required this.title,
required this.coin,
required this.balance,
@ -297,7 +302,7 @@ class BalanceSelector<T> extends ConsumerWidget {
required this.onChanged,
required this.value,
required this.groupValue,
}) : super(key: key);
});
final String title;
final Coin coin;

View file

@ -11,7 +11,6 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/transaction_filter.dart';
import 'package:stackwallet/providers/global/locale_provider.dart';
@ -29,6 +28,7 @@ import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/date_picker/date_picker.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
@ -40,9 +40,9 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart';
class TransactionSearchFilterView extends ConsumerStatefulWidget {
const TransactionSearchFilterView({
Key? key,
super.key,
required this.coin,
}) : super(key: key);
});
static const String routeName = "/transactionSearchFilter";
@ -137,56 +137,6 @@ class _TransactionSearchViewState
DateTime? _selectedFromDate = DateTime(2007);
DateTime? _selectedToDate = DateTime.now();
MaterialRoundedDatePickerStyle _buildDatePickerStyle() {
return MaterialRoundedDatePickerStyle(
backgroundPicker: Theme.of(context).extension<StackColors>()!.popupBG,
// backgroundHeader: Theme.of(context).extension<StackColors>()!.textSubtitle2,
paddingMonthHeader: const EdgeInsets.only(top: 11),
colorArrowNext: Theme.of(context).extension<StackColors>()!.textSubtitle1,
colorArrowPrevious:
Theme.of(context).extension<StackColors>()!.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<StackColors>()!.textSubtitle3,
),
textStyleDayOnCalendarSelected:
STextStyles.datePicker400(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textWhite,
),
textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle1,
),
textStyleYearButton: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textWhite,
),
// textStyleButtonAction: GoogleFonts.inter(),
);
}
MaterialRoundedYearPickerStyle _buildYearPickerStyle() {
return MaterialRoundedYearPickerStyle(
backgroundPicker: Theme.of(context).extension<StackColors>()!.popupBG,
textStyleYear: STextStyles.datePicker600(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle2,
fontSize: 16,
),
textStyleYearSelected: STextStyles.datePicker600(context).copyWith(
fontSize: 18,
),
);
}
Widget _buildDateRangePicker() {
const middleSeparatorPadding = 2.0;
const middleSeparatorWidth = 12.0;
@ -207,58 +157,36 @@ class _TransactionSearchViewState
child: GestureDetector(
key: const Key("transactionSearchViewFromDatePickerKey"),
onTap: () async {
final color =
Theme.of(context).extension<StackColors>()!.accentColorDark;
final height = MediaQuery.of(context).size.height;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 125));
}
final date = await showRoundedDatePicker(
// This doesn't change statusbar color...
// background: CFColors.starryNight.withOpacity(0.8),
context: context,
initialDate: DateTime.now(),
height: height * 0.5,
theme: ThemeData(
primarySwatch: Util.createMaterialColor(
color,
),
),
//TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched
firstDate: DateTime(2007),
lastDate: DateTime.now(),
borderRadius: Constants.size.circularBorderRadius * 2,
if (mounted) {
final date = await showSWDatePicker(context);
if (date != null) {
_selectedFromDate = date;
textPositiveButton: "SELECT",
styleDatePicker: _buildDatePickerStyle(),
styleYearPicker: _buildYearPickerStyle(),
);
if (date != null) {
_selectedFromDate = date;
// flag to adjust date so from date is always before to date
final flag = _selectedToDate != null &&
!_selectedFromDate!.isBefore(_selectedToDate!);
if (flag) {
_selectedToDate = DateTime.fromMillisecondsSinceEpoch(
_selectedFromDate!.millisecondsSinceEpoch);
}
setState(() {
// flag to adjust date so from date is always before to date
final flag = _selectedToDate != null &&
!_selectedFromDate!.isBefore(_selectedToDate!);
if (flag) {
_toDateString = _selectedToDate == null
? ""
: Format.formatDate(_selectedToDate!);
_selectedToDate = DateTime.fromMillisecondsSinceEpoch(
_selectedFromDate!.millisecondsSinceEpoch);
}
_fromDateString = _selectedFromDate == null
? ""
: Format.formatDate(_selectedFromDate!);
});
setState(() {
if (flag) {
_toDateString = _selectedToDate == null
? ""
: Format.formatDate(_selectedToDate!);
}
_fromDateString = _selectedFromDate == null
? ""
: Format.formatDate(_selectedFromDate!);
});
}
}
},
child: Container(
@ -319,58 +247,36 @@ class _TransactionSearchViewState
child: GestureDetector(
key: const Key("transactionSearchViewToDatePickerKey"),
onTap: () async {
final color =
Theme.of(context).extension<StackColors>()!.accentColorDark;
final height = MediaQuery.of(context).size.height;
// check and hide keyboard
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 125));
}
final date = await showRoundedDatePicker(
// This doesn't change statusbar color...
// background: CFColors.starryNight.withOpacity(0.8),
context: context,
height: height * 0.5,
theme: ThemeData(
primarySwatch: Util.createMaterialColor(
color,
),
),
//TODO pick a better initial date
// 2007 chosen as that is just before bitcoin launched
initialDate: DateTime.now(),
firstDate: DateTime(2007),
lastDate: DateTime.now(),
borderRadius: Constants.size.circularBorderRadius * 2,
if (mounted) {
final date = await showSWDatePicker(context);
if (date != null) {
_selectedToDate = date;
textPositiveButton: "SELECT",
styleDatePicker: _buildDatePickerStyle(),
styleYearPicker: _buildYearPickerStyle(),
);
if (date != null) {
_selectedToDate = date;
// flag to adjust date so from date is always before to date
final flag = _selectedFromDate != null &&
!_selectedToDate!.isAfter(_selectedFromDate!);
if (flag) {
_selectedFromDate = DateTime.fromMillisecondsSinceEpoch(
_selectedToDate!.millisecondsSinceEpoch);
}
setState(() {
// flag to adjust date so from date is always before to date
final flag = _selectedFromDate != null &&
!_selectedToDate!.isAfter(_selectedFromDate!);
if (flag) {
_fromDateString = _selectedFromDate == null
? ""
: Format.formatDate(_selectedFromDate!);
_selectedFromDate = DateTime.fromMillisecondsSinceEpoch(
_selectedToDate!.millisecondsSinceEpoch);
}
_toDateString = _selectedToDate == null
? ""
: Format.formatDate(_selectedToDate!);
});
setState(() {
if (flag) {
_fromDateString = _selectedFromDate == null
? ""
: Format.formatDate(_selectedFromDate!);
}
_toDateString = _selectedToDate == null
? ""
: Format.formatDate(_selectedToDate!);
});
}
}
},
child: Container(
@ -454,7 +360,7 @@ class _TransactionSearchViewState
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
if (context.mounted) {
Navigator.of(context).pop();
}
},
@ -908,7 +814,7 @@ class _TransactionSearchViewState
);
}
}
if (mounted) {
if (context.mounted) {
Navigator.of(context).pop();
}
},

View file

@ -23,6 +23,7 @@ import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
@ -44,6 +45,7 @@ class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
late final StreamSubscription<List<TransactionV2>> _subscription;
late final Query<TransactionV2> _query;
late final Coin coin;
BorderRadius get _borderRadiusFirst {
return BorderRadius.only(
@ -69,6 +71,7 @@ class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
@override
void initState() {
coin = ref.read(pWallets).getWallet(widget.walletId).info.coin;
_query = ref
.read(mainDBProvider)
.isar
@ -110,8 +113,6 @@ class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
@override
Widget build(BuildContext context) {
final coin = ref.watch(pWallets).getWallet(widget.walletId).info.coin;
return FutureBuilder(
future: _query.findAll(),
builder: (fbContext, AsyncSnapshot<List<TransactionV2>> snapshot) {

View file

@ -16,6 +16,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/frost_route_generator.dart';
import 'package:stackwallet/models/isar/exchange_cache/currency.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart';
@ -29,6 +30,7 @@ import 'package:stackwallet/pages/ordinals/ordinals_view.dart';
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
import 'package:stackwallet/pages/receive_view/receive_view.dart';
import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart';
import 'package:stackwallet/pages/send_view/send_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
@ -64,6 +66,7 @@ import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
@ -76,12 +79,14 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/frost_scaffold.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/small_tor_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/fusion_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart';
@ -119,7 +124,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
late final bool isSparkWallet;
late final bool _shouldDisableAutoSyncOnLogOut;
// late final bool _shouldDisableAutoSyncOnLogOut;
late WalletSyncStatus _currentSyncStatus;
late NodeConnectionStatus _currentNodeStatus;
@ -165,14 +170,16 @@ class _WalletViewState extends ConsumerState<WalletView> {
final wallet = ref.read(pWallets).getWallet(walletId);
coin = wallet.info.coin;
ref.read(currentWalletIdProvider.notifier).state = wallet.walletId;
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(currentWalletIdProvider.notifier).state = wallet.walletId;
});
if (!wallet.shouldAutoSync) {
// enable auto sync if it wasn't enabled when loading wallet
wallet.shouldAutoSync = true;
_shouldDisableAutoSyncOnLogOut = true;
} else {
_shouldDisableAutoSyncOnLogOut = false;
// _shouldDisableAutoSyncOnLogOut = true;
// } else {
// _shouldDisableAutoSyncOnLogOut = false;
}
isSparkWallet = wallet is SparkInterface;
@ -270,34 +277,36 @@ class _WalletViewState extends ConsumerState<WalletView> {
const timeout = Duration(milliseconds: 1500);
if (_cachedTime == null || now.difference(_cachedTime!) > timeout) {
_cachedTime = now;
unawaited(showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => WillPopScope(
onWillPop: () async {
Navigator.of(context).popUntil(
ModalRoute.withName(HomeView.routeName),
);
_logout();
return false;
},
child: const StackDialog(title: "Tap back again to exit wallet"),
unawaited(
showDialog<dynamic>(
context: context,
barrierDismissible: false,
builder: (_) => WillPopScope(
onWillPop: () async {
Navigator.of(context).popUntil(
ModalRoute.withName(HomeView.routeName),
);
_logout();
return false;
},
child: const StackDialog(title: "Tap back again to exit wallet"),
),
).timeout(
timeout,
onTimeout: () => Navigator.of(context).popUntil(
ModalRoute.withName(WalletView.routeName),
),
),
).timeout(
timeout,
onTimeout: () => Navigator.of(context).popUntil(
ModalRoute.withName(WalletView.routeName),
),
));
);
}
return false;
}
void _logout() async {
if (_shouldDisableAutoSyncOnLogOut) {
// disable auto sync if it was enabled only when loading wallet
ref.read(pWallets).getWallet(walletId).shouldAutoSync = false;
}
// if (_shouldDisableAutoSyncOnLogOut) {
// // disable auto sync if it was enabled only when loading wallet
ref.read(pWallets).getWallet(walletId).shouldAutoSync = false;
// }
ref.read(currentWalletIdProvider.notifier).state = null;
ref.read(transactionFilterProvider.state).state = null;
@ -354,7 +363,27 @@ class _WalletViewState extends ConsumerState<WalletView> {
}
}
void _onExchangePressed(BuildContext context) async {
Future<void> _onFrostSignPressed(BuildContext context) async {
final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet;
ref.read(pFrostScaffoldArgs.state).state = (
info: (
walletName: wallet.info.name,
frostCurrency: wallet.cryptoCurrency,
),
walletId: walletId,
stepRoutes: FrostRouteGenerator.signFrostTxStepRoutes,
parentNav: Navigator.of(context),
frostInterruptionDialogType:
FrostInterruptionDialogType.transactionCreation,
callerRouteName: WalletView.routeName,
);
await Navigator.of(context).pushNamed(
FrostStepScaffold.routeName,
);
}
Future<void> _onExchangePressed(BuildContext context) async {
final Coin coin = ref.read(pWalletCoin(walletId));
if (coin.isTestNet) {
@ -372,11 +401,12 @@ class _WalletViewState extends ConsumerState<WalletView> {
.tickerEqualToAnyExchangeNameName(coin.ticker)
.findFirst();
} catch (_) {
_future = ExchangeDataLoadingService.instance.loadAll().then((_) =>
ExchangeDataLoadingService.instance.isar.currencies
.where()
.tickerEqualToAnyExchangeNameName(coin.ticker)
.findFirst());
_future = ExchangeDataLoadingService.instance.loadAll().then(
(_) => ExchangeDataLoadingService.instance.isar.currencies
.where()
.tickerEqualToAnyExchangeNameName(coin.ticker)
.findFirst(),
);
}
final currency = await showLoading(
@ -385,7 +415,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
message: "Loading...",
);
if (mounted) {
if (context.mounted) {
unawaited(
Navigator.of(context).pushNamed(
WalletInitiatedExchangeView.routeName,
@ -399,7 +429,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
}
}
void _onBuyPressed(BuildContext context) async {
Future<void> _onBuyPressed(BuildContext context) async {
final Coin coin = ref.read(pWalletCoin(walletId));
if (coin.isTestNet) {
@ -542,7 +572,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
},
),
),
)
),
],
),
);
@ -581,7 +611,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
)
),
],
),
actions: [
@ -644,9 +674,12 @@ class _WalletViewState extends ConsumerState<WalletView> {
color: Theme.of(context)
.extension<StackColors>()!
.background,
icon: ref.watch(notificationsProvider.select(
(value) => value
.hasUnreadNotificationsFor(walletId)))
icon: ref.watch(
notificationsProvider.select(
(value) =>
value.hasUnreadNotificationsFor(walletId),
),
)
? SvgPicture.file(
File(
ref.watch(
@ -657,10 +690,14 @@ class _WalletViewState extends ConsumerState<WalletView> {
),
width: 20,
height: 20,
color: ref.watch(notificationsProvider.select(
(value) =>
value.hasUnreadNotificationsFor(
walletId)))
color: ref.watch(
notificationsProvider.select(
(value) =>
value.hasUnreadNotificationsFor(
walletId,
),
),
)
? null
: Theme.of(context)
.extension<StackColors>()!
@ -670,10 +707,14 @@ class _WalletViewState extends ConsumerState<WalletView> {
Assets.svg.bell,
width: 20,
height: 20,
color: ref.watch(notificationsProvider.select(
(value) =>
value.hasUnreadNotificationsFor(
walletId)))
color: ref.watch(
notificationsProvider.select(
(value) =>
value.hasUnreadNotificationsFor(
walletId,
),
),
)
? null
: Theme.of(context)
.extension<StackColors>()!
@ -694,22 +735,25 @@ class _WalletViewState extends ConsumerState<WalletView> {
.state;
if (unreadNotificationIds.isEmpty) return;
List<Future<dynamic>> futures = [];
final List<Future<dynamic>> futures = [];
for (int i = 0;
i < unreadNotificationIds.length - 1;
i++) {
futures.add(ref
.read(notificationsProvider)
.markAsRead(
futures.add(
ref.read(notificationsProvider).markAsRead(
unreadNotificationIds.elementAt(i),
false));
false,
),
);
}
// wait for multiple to update if any
Future.wait(futures).then((_) {
// only notify listeners once
ref.read(notificationsProvider).markAsRead(
unreadNotificationIds.last, true);
unreadNotificationIds.last,
true,
);
});
});
},
@ -798,7 +842,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(
context),
context,
),
onPressed: () async {
await showDialog<void>(
context: context,
@ -829,7 +874,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(
context),
context,
),
child: Text(
"Continue",
style:
@ -974,6 +1020,12 @@ class _WalletViewState extends ConsumerState<WalletView> {
}
},
),
if (ref.watch(pWalletCoin(walletId)).isFrost)
WalletNavigationBarItemData(
label: "Sign",
icon: const FrostSignNavIcon(),
onTap: () => _onFrostSignPressed(context),
),
WalletNavigationBarItemData(
label: "Send",
icon: const SendNavIcon(),
@ -994,21 +1046,26 @@ class _WalletViewState extends ConsumerState<WalletView> {
// break;
// }
Navigator.of(context).pushNamed(
SendView.routeName,
arguments: Tuple2(
walletId,
coin,
ref.read(pWallets).getWallet(walletId)
is BitcoinFrostWallet
? FrostSendView.routeName
: SendView.routeName,
arguments: (
walletId: walletId,
coin: coin,
),
);
},
),
if (Constants.enableExchange)
if (Constants.enableExchange &&
!ref.watch(pWalletCoin(walletId)).isFrost)
WalletNavigationBarItemData(
label: "Swap",
icon: const ExchangeNavIcon(),
onTap: () => _onExchangePressed(context),
),
if (Constants.enableExchange)
if (Constants.enableExchange &&
!ref.watch(pWalletCoin(walletId)).isFrost)
WalletNavigationBarItemData(
label: "Buy",
icon: const BuyNavIcon(),
@ -1036,21 +1093,22 @@ class _WalletViewState extends ConsumerState<WalletView> {
),
if (coin == Coin.banano)
WalletNavigationBarItemData(
icon: SvgPicture.asset(
Assets.svg.monkey,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.bottomNavIconIcon,
),
label: "MonKey",
onTap: () {
Navigator.of(context).pushNamed(
MonkeyView.routeName,
arguments: widget.walletId,
);
}),
icon: SvgPicture.asset(
Assets.svg.monkey,
height: 20,
width: 20,
color: Theme.of(context)
.extension<StackColors>()!
.bottomNavIconIcon,
),
label: "MonKey",
onTap: () {
Navigator.of(context).pushNamed(
MonkeyView.routeName,
arguments: widget.walletId,
);
},
),
if (ref.watch(
pWallets.select(
(value) => value.getWallet(widget.walletId)
@ -1075,8 +1133,12 @@ class _WalletViewState extends ConsumerState<WalletView> {
);
},
),
if (ref.watch(pWallets.select((value) =>
value.getWallet(widget.walletId) is PaynymInterface)))
if (ref.watch(
pWallets.select(
(value) =>
value.getWallet(widget.walletId) is PaynymInterface,
),
))
WalletNavigationBarItemData(
label: "PayNym",
icon: const PaynymNavIcon(),
@ -1108,7 +1170,7 @@ class _WalletViewState extends ConsumerState<WalletView> {
level: LogLevel.Info,
);
if (mounted) {
if (context.mounted) {
Navigator.of(context).pop();
// check if account exists and for matching code to see if claimed

View file

@ -127,7 +127,7 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> {
whileFuture: loadFuture,
context: context,
message: 'Opening ${wallet.info.name}',
isDesktop: Util.isDesktop,
rootNavigator: Util.isDesktop,
);
if (mounted) {

Some files were not shown because too many files have changed in this diff Show more