mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-23 19:05:51 +00:00
Merge pull request #866 from cypherstack/staging
Update to version 2.0.0
This commit is contained in:
commit
f5489feae7
242 changed files with 23464 additions and 8313 deletions
17
.github/workflows/test.yaml
vendored
17
.github/workflows/test.yaml
vendored
|
@ -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
5
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
BIN
assets/images/mascot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 218 KiB |
10
assets/svg/swap2.svg
Normal file
10
assets/svg/swap2.svg
Normal 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
|
1
crypto_plugins/frostdart
Submodule
1
crypto_plugins/frostdart
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit d539de2348bdbb87bac341dcaa6a0755f21d48e2
|
247
docs/building.md
247
docs/building.md
|
@ -4,12 +4,27 @@ Here you will find instructions on how to install the necessary tools for buildi
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- The only OS supported for building Android and Linux desktop is Ubuntu 20.04. Windows build are completed using Ubuntu 20.04 on WSL2. Advanced users may also be able to build on other Debian-based distributions like Linux Mint.
|
||||
- The only OS supported for building Android and Linux desktop is Ubuntu 20.04. Windows builds require using Ubuntu 20.04 on WSL2. macOS builds for itself and iOS. Advanced users may also be able to build on other Debian-based distributions like Linux Mint.
|
||||
- Android setup ([Android Studio](https://developer.android.com/studio) and subsequent dependencies)
|
||||
- 100 GB of storage
|
||||
|
||||
## Linux host
|
||||
The following instructions are for building and running on a Linux host. Alternatively, see the [Windows](#Windows host) section.
|
||||
|
||||
The following instructions are for building and running on a Linux host. Alternatively, see the [Mac](#mac-host) and/or [Windows](#windows-host) section. This entire section (except for the Android Studio section) needs to be completed in WSL if building on a Windows host.
|
||||
|
||||
### Flutter
|
||||
Install Flutter 3.19.6 by [following their guide](https://docs.flutter.dev/get-started/install/linux/desktop?tab=download#install-the-flutter-sdk). You can also clone https://github.com/flutter/flutter, check out the `3.19.6` tag, and add its `flutter/bin` folder to your PATH as in
|
||||
```sh
|
||||
FLUTTER_DIR="$HOME/development/flutter"
|
||||
git clone https://github.com/flutter/flutter.git "$FLUTTER_DIR"
|
||||
cd "$FLUTTER_DIR"
|
||||
git checkout 3.16.9
|
||||
echo 'export PATH="$PATH:'"$FLUTTER_DIR"'/bin"' >> "$HOME/.profile"
|
||||
source "$HOME/.profile"
|
||||
flutter precache
|
||||
```
|
||||
|
||||
Run `flutter doctor` in a terminal to confirm its installation.
|
||||
|
||||
### Android Studio
|
||||
Install Android Studio. Follow instructions here [https://developer.android.com/studio/install#linux](https://developer.android.com/studio/install#linux) or install via snap:
|
||||
|
@ -20,42 +35,46 @@ sudo snap install android-studio --classic
|
|||
```
|
||||
|
||||
Use `Tools > SDK Manager` to install:
|
||||
- `SDK Tools > Android SDK (API 30)`
|
||||
- `SDK Tools > NDK`
|
||||
- `SDK Tools > Android SDK command line tools`
|
||||
- `SDK Tools > CMake`
|
||||
and for Android builds,
|
||||
- `SDK Tools > Android SDK (API 30)`
|
||||
- `SDK Tools > NDK`
|
||||
|
||||
Then in `File > Settings > Plugins`, install the **Flutter** and **Dart** plugins and restart the IDE. In `File > Settings > Languages & Frameworks > Flutter > Editor`, enable auto format on save to match the project's code style. If you have problems with the Dart SDK, make sure to run `flutter` in a terminal to download it (use `source ~/.bashrc` to update your environment variables if you're still using the same terminal from which you ran `setup.sh`). Run `flutter doctor` to install any missing dependencies and review and agree to any license agreements.
|
||||
|
||||
Make a Pixel 4 (API 30) x86_64 emulator with 2GB of storage space for emulation
|
||||
|
||||
Install basic dependencies
|
||||
```
|
||||
sudo apt-get install libssl-dev curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm python3-distutils
|
||||
```
|
||||
Make a Pixel 4 (API 30) x86_64 emulator with 2GB of storage space for emulation.
|
||||
|
||||
The following *may* be needed for Android studio:
|
||||
```
|
||||
sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2-1.0:i386
|
||||
```
|
||||
|
||||
### Build dependencies
|
||||
Install basic dependencies
|
||||
```
|
||||
sudo apt-get install libssl-dev curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm python3-distutils
|
||||
```
|
||||
|
||||
Install [Rust](https://www.rust-lang.org/tools/install) with command:
|
||||
```
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source ~/.bashrc
|
||||
rustup install 1.67.1
|
||||
rustup install 1.67.1 1.72.0 1.73.0
|
||||
rustup default 1.67.1
|
||||
```
|
||||
|
||||
Install the additional components for Rust:
|
||||
```
|
||||
cargo install cargo-ndk --version 2.12.7
|
||||
cargo install cargo-ndk --version 2.12.7 --locked
|
||||
```
|
||||
|
||||
Android specific dependencies:
|
||||
```
|
||||
sudo apt-get install libc6-dev-i386
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
|
||||
```
|
||||
|
||||
Linux desktop specific dependencies:
|
||||
```
|
||||
sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev meson python3-pip libgirepository1.0-dev valac xsltproc docbook-xsl
|
||||
|
@ -67,18 +86,27 @@ After installing the prerequisites listed above, download the code and init the
|
|||
git clone https://github.com/cypherstack/stack_wallet.git
|
||||
cd stack_wallet
|
||||
git submodule update --init --recursive
|
||||
|
||||
```
|
||||
|
||||
Remove pre-installed system libraries for the following packages built by cryptography plugins in the crypto_plugins folder: `boost iconv libjson-dev libsecret openssl sodium unbound zmq`. You can use
|
||||
Build the secure storage dependencies in order to target Linux (not needed for Windows or other platforms):
|
||||
```
|
||||
sudo apt list --installed | grep boost
|
||||
cd scripts/linux
|
||||
./build_secure_storage_deps.sh
|
||||
// when finished go back to the root directory
|
||||
cd ../..
|
||||
```
|
||||
for example to find which pre-installed packages you may need to remove with `sudo apt remove`. Be careful, as some packages (especially boost) are linked to GNOME (GUI) packages: when in doubt, remove `-dev` packages first like with
|
||||
```
|
||||
sudo apt-get remove '^libboost.*-dev.*'
|
||||
```
|
||||
<!-- 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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
96
lib/electrumx_rpc/client_manager.dart
Normal file
96
lib/electrumx_rpc/client_manager.dart
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
275
lib/frost_route_generator.dart
Normal file
275
lib/frost_route_generator.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -687,6 +687,7 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
|
|||
appBarTheme: AppBarTheme(
|
||||
centerTitle: false,
|
||||
color: colorScheme.background,
|
||||
surfaceTintColor: colorScheme.background,
|
||||
elevation: 0,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
|
|
|
@ -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)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
"}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>()!
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
572
lib/pages/send_view/frost_ms/frost_send_view.dart
Normal file
572
lib/pages/send_view/frost_ms/frost_send_view.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
447
lib/pages/send_view/frost_ms/recipient.dart
Normal file
447
lib/pages/send_view/frost_ms/recipient.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
236
lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart
Normal file
236
lib/pages/send_view/frost_ms/send_steps/frost_send_step_1a.dart
Normal 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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
198
lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart
Normal file
198
lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart
Normal 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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
311
lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart
Normal file
311
lib/pages/send_view/frost_ms/send_steps/frost_send_step_2.dart
Normal 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",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
253
lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart
Normal file
253
lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart
Normal 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",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
296
lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart
Normal file
296
lib/pages/send_view/frost_ms/send_steps/frost_send_step_4.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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>()!
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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:")) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}",
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue